初始化仓库

This commit is contained in:
2026-06-02 23:14:41 +08:00
commit 0bc3f02670
520 changed files with 191097 additions and 0 deletions
+906
View File
@@ -0,0 +1,906 @@
# Bridge Platform Protocol Specification
> Version: 1.0-draft
> Status: Draft — subject to change before implementation
## Overview
The Bridge Protocol allows **external platform adapters** written in any programming language to connect to cc-connect at runtime via WebSocket. This eliminates the requirement to write Go code and recompile the binary for every new platform integration.
### Architecture
```
┌──────────────────────────────────────────────────────┐
│ cc-connect │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────────┐ │
│ │ Telegram │ │ Feishu │ │ BridgePlatform │ │
│ │ (native) │ │ (native) │ │ (WebSocket) │ │
│ └─────┬──────┘ └─────┬──────┘ └───────┬────────┘ │
│ │ │ │ │
│ └──────────────┴────────────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ Engine │ │
│ └───────────┘ │
└──────────────────────────────────────────────────────┘
│ WebSocket
┌──────────┴───────────┐
│ │
┌──────────┴──────┐ ┌───────────┴─────┐
│ Python Adapter │ │ Node.js Adapter │
│ (WeChat, Line…) │ │ (Custom Chat…) │
└─────────────────┘ └─────────────────┘
```
The `BridgePlatform` is a built-in platform inside cc-connect that:
1. Exposes a WebSocket endpoint for external adapters to connect.
2. Translates WebSocket messages into `core.Platform` interface calls.
3. Routes engine replies back to the adapter over the same WebSocket connection.
---
## Connection
### Endpoint
```
ws://<host>:<port>/bridge/ws
```
The port and path are configured in `config.toml`:
```toml
[bridge]
enabled = true
port = 9810
path = "/bridge/ws" # optional, default "/bridge/ws"
token = "your-secret" # required for authentication
```
### Authentication
The adapter must authenticate on connection using one of:
| Method | Example |
|--------|---------|
| Query parameter | `ws://host:9810/bridge/ws?token=your-secret` |
| Header | `Authorization: Bearer your-secret` |
| Header | `X-Bridge-Token: your-secret` |
Unauthenticated connections are rejected with HTTP 401.
### Connection Lifecycle
```
Adapter cc-connect
│ │
│──── WebSocket Connect ──────────→│ (with token)
│ │
│──── register ──────────────────→│ (declare platform name & capabilities)
│←─── register_ack ──────────────│ (confirm or reject)
│ │
│←──→ message / reply exchange ──→│ (bidirectional)
│ │
│──── ping ──────────────────────→│ (keepalive, every 30s recommended)
│←─── pong ──────────────────────│
│ │
│──── close ─────────────────────→│ (graceful disconnect)
```
---
## Message Protocol
All messages are JSON objects with a required `type` field. The protocol uses newline-delimited JSON over WebSocket text frames (one JSON object per frame).
### Adapter → cc-connect
#### `register`
Must be the first message after connection. Declares the adapter identity and capabilities.
```json
{
"type": "register",
"platform": "wechat",
"capabilities": ["text", "image", "file", "audio", "card", "buttons", "typing", "update_message", "preview"],
"metadata": {
"version": "1.0.0",
"description": "WeChat Official Account adapter"
}
}
```
**Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | yes | `"register"` |
| `platform` | string | yes | Unique platform name (lowercase, alphanumeric + hyphens). Used in session keys. |
| `capabilities` | string[] | yes | List of supported capabilities (see [Capabilities](#capabilities)). |
| `metadata` | object | no | Free-form metadata for logging/debugging. |
#### `message`
Delivers an incoming user message to the engine.
```json
{
"type": "message",
"msg_id": "msg-001",
"session_key": "wechat:user123:user123",
"user_id": "user123",
"user_name": "Alice",
"content": "Hello, what can you do?",
"reply_ctx": "conv-abc-123",
"images": [],
"files": [],
"audio": null
}
```
**Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | yes | `"message"` |
| `msg_id` | string | yes | Platform-specific message ID for tracing. |
| `session_key` | string | yes | Unique session identifier. Format: `{platform}:{scope}:{user}`. The adapter defines how to compose this. |
| `user_id` | string | yes | User identifier on the platform. |
| `user_name` | string | no | Display name. |
| `content` | string | yes | Text content. |
| `reply_ctx` | string | yes | Opaque context string the adapter needs to route replies back. cc-connect echoes this in every reply. |
| `images` | Image[] | no | Attached images (see [Image Object](#image-object)). |
| `files` | File[] | no | Attached files (see [File Object](#file-object)). |
| `audio` | Audio | no | Voice message (see [Audio Object](#audio-object)). |
#### `card_action`
User clicked a button or selected an option on a card.
```json
{
"type": "card_action",
"session_key": "wechat:user123:user123",
"action": "cmd:/new",
"reply_ctx": "conv-abc-123"
}
```
**Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | yes | `"card_action"` |
| `session_key` | string | yes | Session that triggered the action. |
| `action` | string | yes | The callback value from the button (e.g., `"cmd:/new"`, `"nav:/model"`, `"act:/heartbeat pause"`). |
| `reply_ctx` | string | yes | Reply context for routing the response. |
#### `preview_ack`
Acknowledges a preview start and returns a handle for subsequent updates.
```json
{
"type": "preview_ack",
"ref_id": "preview-req-001",
"preview_handle": "platform-msg-id-789"
}
```
#### `ping`
Keepalive. cc-connect responds with `pong`.
```json
{
"type": "ping",
"ts": 1710000000000
}
```
---
### cc-connect → Adapter
#### `register_ack`
Confirms or rejects registration.
```json
{
"type": "register_ack",
"ok": true,
"error": ""
}
```
#### `reply`
A complete reply message to send to the user.
```json
{
"type": "reply",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"content": "I can help you with coding tasks!",
"format": "text"
}
```
**Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | yes | `"reply"` |
| `session_key` | string | yes | Target session. |
| `reply_ctx` | string | yes | Echoed from the original message. |
| `content` | string | yes | Reply text content. |
| `format` | string | no | `"text"` (default) or `"markdown"`. |
#### `reply_stream`
Streaming delta for real-time typing preview. Only sent if the adapter declared `"preview"` capability.
```json
{
"type": "reply_stream",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"delta": "partial content...",
"full_text": "accumulated full text so far...",
"preview_handle": "platform-msg-id-789",
"done": false
}
```
| Field | Type | Description |
|-------|------|-------------|
| `delta` | string | New text since last stream message. |
| `full_text` | string | Full accumulated text. Adapters can use this for "replace entire message" updates. |
| `preview_handle` | string | Handle returned by `preview_ack`. Empty on first stream message. |
| `done` | bool | `true` on the final stream message. |
#### `preview_start`
Requests the adapter to create an initial preview message (for streaming).
```json
{
"type": "preview_start",
"ref_id": "preview-req-001",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"content": "Thinking..."
}
```
The adapter should send the message and respond with `preview_ack` containing the platform message ID.
#### `update_message`
Requests the adapter to edit an existing message in-place. Used for streaming preview updates.
```json
{
"type": "update_message",
"session_key": "wechat:user123:user123",
"preview_handle": "platform-msg-id-789",
"content": "Updated text content..."
}
```
#### `delete_message`
Requests the adapter to delete a message (e.g., cleaning up preview messages).
```json
{
"type": "delete_message",
"session_key": "wechat:user123:user123",
"preview_handle": "platform-msg-id-789"
}
```
#### `card`
Send a structured card to the user. Only sent if the adapter declared `"card"` capability; otherwise cc-connect falls back to `reply` with `card.RenderText()`.
```json
{
"type": "card",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"card": {
"header": {
"title": "Model Selection",
"color": "blue"
},
"elements": [
{
"type": "markdown",
"content": "Choose a model:"
},
{
"type": "actions",
"buttons": [
{"text": "GPT-4", "btn_type": "primary", "value": "cmd:/model switch gpt-4"},
{"text": "Claude", "btn_type": "default", "value": "cmd:/model switch claude"}
],
"layout": "row"
},
{
"type": "divider"
},
{
"type": "note",
"text": "Current: gpt-4"
}
]
}
}
```
See [Card Schema](#card-schema) for the full card element reference.
#### `buttons`
Send a message with inline buttons. Only sent if the adapter declared `"buttons"` capability.
```json
{
"type": "buttons",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"content": "Allow tool execution: bash(rm -rf /tmp/old)?",
"buttons": [
[
{"text": "✅ Allow", "data": "perm:req-123:allow"},
{"text": "❌ Deny", "data": "perm:req-123:deny"}
]
]
}
```
`buttons` is a 2D array: each inner array is one row.
#### `typing_start`
Requests the adapter to show a typing indicator.
```json
{
"type": "typing_start",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123"
}
```
#### `typing_stop`
Requests the adapter to hide the typing indicator.
```json
{
"type": "typing_stop",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123"
}
```
#### `audio`
Send a voice/audio message. Only sent if the adapter declared `"audio"` capability.
```json
{
"type": "audio",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64-encoded-audio>",
"format": "mp3"
}
```
#### `image`
Send an image to the user. Only sent if the adapter declared `"image"` capability.
```json
{
"type": "image",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64-encoded-image>",
"mime_type": "image/png",
"file_name": "screenshot.png"
}
```
#### `file`
Send a file to the user. Only sent if the adapter declared `"file"` capability.
```json
{
"type": "file",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64-encoded-file>",
"mime_type": "application/pdf",
"file_name": "report.pdf"
}
```
#### `pong`
Response to `ping`.
```json
{
"type": "pong",
"ts": 1710000000000
}
```
#### `error`
Notify the adapter of a server-side error.
```json
{
"type": "error",
"code": "session_not_found",
"message": "No active session for the given key"
}
```
---
## Data Schemas
### Capabilities
| Capability | Description | Enables |
|------------|-------------|---------|
| `text` | Basic text messaging (required) | `message`, `reply` |
| `image` | Sending/receiving images | `message.images`, `image` reply |
| `file` | Sending/receiving files | `message.files`, `file` reply |
| `audio` | Sending/receiving voice messages | `message.audio`, `audio` reply |
| `card` | Structured rich card rendering | `card` reply |
| `buttons` | Inline clickable buttons | `buttons` reply, `card_action` |
| `typing` | Typing indicator | `typing_start`, `typing_stop` |
| `update_message` | Edit existing messages | `update_message` |
| `preview` | Streaming preview (requires `update_message`) | `preview_start`, `reply_stream` |
| `delete_message` | Delete messages | `delete_message` |
| `reconstruct_reply` | Can reconstruct reply context from session_key | Enables cron/heartbeat messages |
If a capability is not declared, cc-connect will automatically degrade:
- No `card` → cards are rendered as plain text via `RenderText()`.
- No `buttons` → buttons are omitted or rendered as text hints.
- No `preview` → streaming is disabled; only the final reply is sent.
- No `typing` → typing indicators are skipped.
### Image Object
```json
{
"mime_type": "image/png",
"data": "<base64-encoded>",
"file_name": "screenshot.png"
}
```
### File Object
```json
{
"mime_type": "application/pdf",
"data": "<base64-encoded>",
"file_name": "report.pdf"
}
```
### Audio Object
```json
{
"mime_type": "audio/ogg",
"data": "<base64-encoded>",
"format": "ogg",
"duration": 5
}
```
### Card Schema
A card consists of an optional header and a list of elements:
```json
{
"header": {
"title": "Card Title",
"color": "blue"
},
"elements": [ ... ]
}
```
**Supported colors:** `blue`, `green`, `red`, `orange`, `purple`, `grey`, `turquoise`, `violet`, `indigo`, `wathet`, `yellow`, `carmine`.
#### Element Types
**Markdown**
```json
{"type": "markdown", "content": "**Bold** and _italic_"}
```
**Divider**
```json
{"type": "divider"}
```
**Actions (Button Row)**
```json
{
"type": "actions",
"buttons": [
{"text": "Click Me", "btn_type": "primary", "value": "cmd:/do-something"}
],
"layout": "row"
}
```
`btn_type`: `"primary"`, `"default"`, `"danger"`.
`layout`: `"row"` (default), `"equal_columns"`.
**List Item (Description + Button)**
```json
{
"type": "list_item",
"text": "GPT-4 — Most capable model",
"btn_text": "Select",
"btn_type": "primary",
"btn_value": "cmd:/model switch gpt-4"
}
```
**Select (Dropdown)**
```json
{
"type": "select",
"placeholder": "Choose a model",
"options": [
{"text": "GPT-4", "value": "cmd:/model switch gpt-4"},
{"text": "Claude", "value": "cmd:/model switch claude"}
],
"init_value": "cmd:/model switch gpt-4"
}
```
**Note (Footnote)**
```json
{
"type": "note",
"text": "Tip: use /help to see all commands",
"tag": "optional-machine-tag"
}
```
---
## Session Key Format
Session keys follow the pattern:
```
{platform}:{scope}:{user_id}
```
- **platform**: The `platform` name from registration (e.g., `wechat`).
- **scope**: A grouping scope — could be a group/channel ID, or the same as `user_id` for 1-on-1 chats.
- **user_id**: The unique user identifier.
Examples:
- `wechat:user123:user123` — personal DM
- `wechat:group456:user123` — user in a group chat
- `matrix:room789:alice` — Matrix room
The adapter is responsible for constructing consistent session keys.
---
## Session Management REST API
In addition to the WebSocket protocol for real-time messaging, the Bridge Server exposes HTTP REST endpoints on the same port for session management. This allows adapters to list, create, switch, and delete sessions without requiring the separate Management API.
### Authentication
The same token used for WebSocket connections applies to REST endpoints:
| Method | Example |
|--------|---------|
| Header | `Authorization: Bearer your-secret` |
| Query param | `?token=your-secret` |
### Response Format
All responses use the same envelope as the Management API:
```json
{"ok": true, "data": { ... }}
{"ok": false, "error": "message"}
```
### Endpoints
All endpoints are relative to the Bridge Server base URL (e.g., `http://localhost:9810`).
#### GET /bridge/sessions
Lists sessions for a given session key prefix (typically `platform:chatId`).
**Query parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `session_key` | string | yes | The session key to list sessions for (e.g., `wechat:user123:user123`). |
**Response:**
```json
{
"ok": true,
"data": {
"sessions": [
{
"id": "s1",
"name": "default",
"history_count": 12
},
{
"id": "s2",
"name": "work",
"history_count": 5
}
],
"active_session_id": "s1"
}
}
```
---
#### POST /bridge/sessions
Creates a new named session.
**Request body:**
```json
{
"session_key": "wechat:user123:user123",
"name": "work"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `session_key` | string | yes | Session key for the user. |
| `name` | string | no | Human-readable session name. Defaults to `"default"`. |
**Response:**
```json
{
"ok": true,
"data": {
"id": "s3",
"name": "work",
"message": "session created"
}
}
```
---
#### GET /bridge/sessions/{id}
Returns session detail with message history.
**Query parameters:**
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `session_key` | string | (required) | Session key to identify the project context. |
| `history_limit` | int | 50 | Max history entries to return. |
**Response:**
```json
{
"ok": true,
"data": {
"id": "s1",
"name": "default",
"history": [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi! How can I help?"}
]
}
}
```
---
#### DELETE /bridge/sessions/{id}
Deletes a session and its history.
**Query parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `session_key` | string | yes | Session key to identify the project context. |
**Response:**
```json
{
"ok": true,
"data": {
"message": "session deleted"
}
}
```
---
#### POST /bridge/sessions/switch
Switches the active session for a session key.
**Request body:**
```json
{
"session_key": "wechat:user123:user123",
"target": "s2"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `session_key` | string | yes | Session key. |
| `target` | string | yes | Session ID or name to switch to. |
**Response:**
```json
{
"ok": true,
"data": {
"message": "session switched",
"active_session_id": "s2"
}
}
```
---
## Error Handling
### Reconnection
If the WebSocket connection drops, the adapter should:
1. Wait with exponential backoff (starting at 1s, max 60s).
2. Reconnect and send a new `register` message.
3. Resume normal operation — cc-connect maintains session state independently of the connection.
### Message Ordering
Messages within a single WebSocket connection are ordered. cc-connect processes adapter messages sequentially per session key.
### Timeouts
- **Ping interval**: Adapters should send `ping` at least every 30 seconds.
- **Connection timeout**: cc-connect closes idle connections after 90 seconds without a ping.
- **Reply timeout**: If an agent takes too long, cc-connect may send an error reply. The adapter does not need to handle this specially.
---
## Configuration Example
```toml
[bridge]
enabled = true
port = 9810
token = "a-strong-random-secret"
# Optional: restrict which adapters can connect (by platform name).
# Default: allow all registered adapters.
# allow_platforms = ["wechat", "matrix"]
```
No per-adapter project configuration is needed — adapters are associated with the **default project** or specify a `project` field in the `register` message to bind to a specific project.
---
## SDK Guidelines
When building an adapter, follow these guidelines:
1. **Keep it stateless** — the adapter should be a thin translation layer. All session state lives in cc-connect.
2. **Handle reconnection** — network failures are normal. Implement exponential backoff.
3. **Declare capabilities honestly** — only declare capabilities your platform actually supports.
4. **Use `reply_ctx` faithfully** — always echo back the `reply_ctx` from the original message.
5. **Base64 for binary data** — images, files, and audio are transferred as base64-encoded strings.
6. **Log errors, don't crash** — if you receive an unknown message type, log it and continue.
### Minimal Adapter Example (Python pseudocode)
```python
import asyncio
import json
import websockets
async def main():
uri = "ws://localhost:9810/bridge/ws?token=your-secret"
async with websockets.connect(uri) as ws:
# 1. Register
await ws.send(json.dumps({
"type": "register",
"platform": "my-chat",
"capabilities": ["text", "buttons"]
}))
ack = json.loads(await ws.recv())
assert ack["ok"], f"Registration failed: {ack['error']}"
# 2. Start message loop
async def recv_loop():
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "reply":
send_to_chat_platform(msg["reply_ctx"], msg["content"])
elif msg["type"] == "buttons":
send_buttons_to_chat(msg["reply_ctx"], msg["content"], msg["buttons"])
# ... handle other types
async def send_loop():
while True:
chat_msg = await get_next_chat_message()
await ws.send(json.dumps({
"type": "message",
"msg_id": chat_msg.id,
"session_key": f"my-chat:{chat_msg.user_id}:{chat_msg.user_id}",
"user_id": chat_msg.user_id,
"user_name": chat_msg.user_name,
"content": chat_msg.text,
"reply_ctx": chat_msg.conversation_id
}))
await asyncio.gather(recv_loop(), send_loop())
asyncio.run(main())
```
---
## Versioning
The protocol version is declared in the `register` message via `metadata.protocol_version`. The current version is `1`. cc-connect will reject connections with incompatible versions and respond with a `register_ack` containing an error.
```json
{
"type": "register",
"platform": "my-chat",
"capabilities": ["text"],
"metadata": {
"protocol_version": 1
}
}
```
+906
View File
@@ -0,0 +1,906 @@
# Bridge 平台协议规范
> 版本:1.0-draft
> 状态:草案 — 实现前可能调整
## 概述
Bridge 协议允许使用**任何编程语言**编写的外部平台适配器在运行时通过 WebSocket 动态接入 cc-connect,无需编写 Go 代码或重新编译二进制文件。
### 架构
```
┌──────────────────────────────────────────────────────┐
│ cc-connect │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────────┐ │
│ │ Telegram │ │ 飞书 │ │ BridgePlatform │ │
│ │ (原生) │ │ (原生) │ │ (WebSocket) │ │
│ └─────┬──────┘ └─────┬──────┘ └───────┬────────┘ │
│ │ │ │ │
│ └──────────────┴────────────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ Engine │ │
│ └───────────┘ │
└──────────────────────────────────────────────────────┘
│ WebSocket
┌──────────┴───────────┐
│ │
┌──────────┴──────┐ ┌───────────┴─────┐
│ Python 适配器 │ │ Node.js 适配器 │
│ (微信公众号等) │ │ (自定义聊天等) │
└─────────────────┘ └─────────────────┘
```
`BridgePlatform` 是 cc-connect 内置的一个平台实现,它:
1. 暴露 WebSocket 端点供外部适配器连接。
2. 将 WebSocket 消息转换为 `core.Platform` 接口调用。
3. 将 Engine 的回复通过同一个 WebSocket 连接推送回适配器。
---
## 连接
### 端点
```
ws://<host>:<port>/bridge/ws
```
端口和路径通过 `config.toml` 配置:
```toml
[bridge]
enabled = true
port = 9810
path = "/bridge/ws" # 可选,默认 "/bridge/ws"
token = "your-secret" # 认证密钥,必填
```
### 认证
适配器连接时必须通过以下方式之一进行身份验证:
| 方式 | 示例 |
|------|------|
| URL 查询参数 | `ws://host:9810/bridge/ws?token=your-secret` |
| 请求头 | `Authorization: Bearer your-secret` |
| 请求头 | `X-Bridge-Token: your-secret` |
未认证的连接将被拒绝并返回 HTTP 401。
### 连接生命周期
```
适配器 cc-connect
│ │
│──── WebSocket 连接 ─────────────→│ (携带 token)
│ │
│──── register ──────────────────→│ (声明平台名和能力)
│←─── register_ack ──────────────│ (确认或拒绝)
│ │
│←──→ message / reply 消息交换 ──→│ (双向)
│ │
│──── ping ──────────────────────→│ (心跳保活,建议 30 秒)
│←─── pong ──────────────────────│
│ │
│──── close ─────────────────────→│ (优雅断开)
```
---
## 消息协议
所有消息均为 JSON 对象,必须包含 `type` 字段。协议使用 WebSocket 文本帧传输(每帧一个 JSON 对象)。
### 适配器 → cc-connect
#### `register`
连接后必须发送的第一条消息。声明适配器身份和支持的能力。
```json
{
"type": "register",
"platform": "wechat",
"capabilities": ["text", "image", "file", "audio", "card", "buttons", "typing", "update_message", "preview"],
"metadata": {
"version": "1.0.0",
"description": "微信公众号适配器"
}
}
```
**字段说明:**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `type` | string | 是 | `"register"` |
| `platform` | string | 是 | 唯一平台名称(小写字母、数字、连字符)。用于组成 session key。 |
| `capabilities` | string[] | 是 | 支持的能力列表(见[能力声明](#能力声明))。 |
| `metadata` | object | 否 | 自由格式的元信息,用于日志/调试。 |
#### `message`
将用户消息传递给引擎。
```json
{
"type": "message",
"msg_id": "msg-001",
"session_key": "wechat:user123:user123",
"user_id": "user123",
"user_name": "Alice",
"content": "你好,你能做什么?",
"reply_ctx": "conv-abc-123",
"images": [],
"files": [],
"audio": null
}
```
**字段说明:**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `type` | string | 是 | `"message"` |
| `msg_id` | string | 是 | 平台消息 ID,用于追踪。 |
| `session_key` | string | 是 | 唯一会话标识。格式:`{platform}:{scope}:{user}`。由适配器定义组合方式。 |
| `user_id` | string | 是 | 用户在平台上的唯一标识。 |
| `user_name` | string | 否 | 显示名称。 |
| `content` | string | 是 | 文本内容。 |
| `reply_ctx` | string | 是 | 不透明的上下文字符串,适配器需要它来路由回复。cc-connect 会在每个回复中原样回传。 |
| `images` | Image[] | 否 | 附带的图片(见[图片对象](#图片对象))。 |
| `files` | File[] | 否 | 附带的文件(见[文件对象](#文件对象))。 |
| `audio` | Audio | 否 | 语音消息(见[音频对象](#音频对象))。 |
#### `card_action`
用户点击了卡片上的按钮或选择了选项。
```json
{
"type": "card_action",
"session_key": "wechat:user123:user123",
"action": "cmd:/new",
"reply_ctx": "conv-abc-123"
}
```
**字段说明:**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `type` | string | 是 | `"card_action"` |
| `session_key` | string | 是 | 触发操作的会话。 |
| `action` | string | 是 | 按钮的回调值(如 `"cmd:/new"``"nav:/model"``"act:/heartbeat pause"`)。 |
| `reply_ctx` | string | 是 | 用于路由响应的回复上下文。 |
#### `preview_ack`
确认预览消息已创建,返回用于后续更新的 handle。
```json
{
"type": "preview_ack",
"ref_id": "preview-req-001",
"preview_handle": "platform-msg-id-789"
}
```
#### `ping`
心跳保活。cc-connect 回应 `pong`
```json
{
"type": "ping",
"ts": 1710000000000
}
```
---
### cc-connect → 适配器
#### `register_ack`
确认或拒绝注册。
```json
{
"type": "register_ack",
"ok": true,
"error": ""
}
```
#### `reply`
发送完整回复消息给用户。
```json
{
"type": "reply",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"content": "我可以帮你完成编码任务!",
"format": "text"
}
```
**字段说明:**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `type` | string | 是 | `"reply"` |
| `session_key` | string | 是 | 目标会话。 |
| `reply_ctx` | string | 是 | 来自原始消息的回传。 |
| `content` | string | 是 | 回复文本内容。 |
| `format` | string | 否 | `"text"`(默认)或 `"markdown"`。 |
#### `reply_stream`
流式增量内容,用于实时打字预览。仅在适配器声明了 `"preview"` 能力时发送。
```json
{
"type": "reply_stream",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"delta": "部分内容...",
"full_text": "累积的完整文本...",
"preview_handle": "platform-msg-id-789",
"done": false
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `delta` | string | 自上次流式消息以来的新增文本。 |
| `full_text` | string | 完整累积文本。适配器可用于"替换整条消息"的更新方式。 |
| `preview_handle` | string | 由 `preview_ack` 返回的 handle。首条流式消息时为空。 |
| `done` | bool | 最后一条流式消息时为 `true`。 |
#### `preview_start`
请求适配器创建初始预览消息(用于流式输出)。
```json
{
"type": "preview_start",
"ref_id": "preview-req-001",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"content": "思考中..."
}
```
适配器应发送消息后回应 `preview_ack`,包含平台消息 ID。
#### `update_message`
请求适配器原地编辑已有消息。用于流式预览更新。
```json
{
"type": "update_message",
"session_key": "wechat:user123:user123",
"preview_handle": "platform-msg-id-789",
"content": "更新后的文本内容..."
}
```
#### `delete_message`
请求适配器删除消息(如清理预览消息)。
```json
{
"type": "delete_message",
"session_key": "wechat:user123:user123",
"preview_handle": "platform-msg-id-789"
}
```
#### `card`
发送结构化卡片给用户。仅在适配器声明了 `"card"` 能力时发送;否则 cc-connect 会降级为 `reply`,内容使用 `card.RenderText()` 生成的纯文本。
```json
{
"type": "card",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"card": {
"header": {
"title": "模型选择",
"color": "blue"
},
"elements": [
{
"type": "markdown",
"content": "请选择一个模型:"
},
{
"type": "actions",
"buttons": [
{"text": "GPT-4", "btn_type": "primary", "value": "cmd:/model switch gpt-4"},
{"text": "Claude", "btn_type": "default", "value": "cmd:/model switch claude"}
],
"layout": "row"
},
{
"type": "divider"
},
{
"type": "note",
"text": "当前模型:gpt-4"
}
]
}
}
```
完整卡片元素参见[卡片 Schema](#卡片-schema)。
#### `buttons`
发送带有内联按钮的消息。仅在适配器声明了 `"buttons"` 能力时发送。
```json
{
"type": "buttons",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"content": "允许执行工具:bash(rm -rf /tmp/old)",
"buttons": [
[
{"text": "✅ 允许", "data": "perm:req-123:allow"},
{"text": "❌ 拒绝", "data": "perm:req-123:deny"}
]
]
}
```
`buttons` 是二维数组:每个内层数组是一行按钮。
#### `typing_start`
请求适配器显示"正在输入"指示器。
```json
{
"type": "typing_start",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123"
}
```
#### `typing_stop`
请求适配器隐藏"正在输入"指示器。
```json
{
"type": "typing_stop",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123"
}
```
#### `audio`
发送语音/音频消息。仅在适配器声明了 `"audio"` 能力时发送。
```json
{
"type": "audio",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64 编码的音频数据>",
"format": "mp3"
}
```
#### `image`
发送图片给用户。仅在适配器声明了 `"image"` 能力时发送。
```json
{
"type": "image",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64 编码的图片数据>",
"mime_type": "image/png",
"file_name": "screenshot.png"
}
```
#### `file`
发送文件给用户。仅在适配器声明了 `"file"` 能力时发送。
```json
{
"type": "file",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64 编码的文件数据>",
"mime_type": "application/pdf",
"file_name": "report.pdf"
}
```
#### `pong`
`ping` 的回应。
```json
{
"type": "pong",
"ts": 1710000000000
}
```
#### `error`
通知适配器服务端错误。
```json
{
"type": "error",
"code": "session_not_found",
"message": "找不到给定 key 的活跃会话"
}
```
---
## 数据 Schema
### 能力声明
| 能力 | 说明 | 启用的消息类型 |
|------|------|--------------|
| `text` | 基础文本消息(必须) | `message``reply` |
| `image` | 收发图片 | `message.images``image` 回复 |
| `file` | 收发文件 | `message.files``file` 回复 |
| `audio` | 收发语音消息 | `message.audio``audio` 回复 |
| `card` | 结构化富卡片渲染 | `card` 回复 |
| `buttons` | 可点击的内联按钮 | `buttons` 回复、`card_action` |
| `typing` | 正在输入指示器 | `typing_start``typing_stop` |
| `update_message` | 编辑已有消息 | `update_message` |
| `preview` | 流式预览(需要 `update_message` | `preview_start``reply_stream` |
| `delete_message` | 删除消息 | `delete_message` |
| `reconstruct_reply` | 可从 session_key 重建回复上下文 | 启用定时任务/心跳消息 |
如果未声明某个能力,cc-connect 会自动降级:
- 没有 `card` → 卡片通过 `RenderText()` 渲染为纯文本。
- 没有 `buttons` → 按钮被省略或渲染为文本提示。
- 没有 `preview` → 禁用流式预览;只发送最终回复。
- 没有 `typing` → 跳过输入指示器。
### 图片对象
```json
{
"mime_type": "image/png",
"data": "<base64 编码>",
"file_name": "screenshot.png"
}
```
### 文件对象
```json
{
"mime_type": "application/pdf",
"data": "<base64 编码>",
"file_name": "report.pdf"
}
```
### 音频对象
```json
{
"mime_type": "audio/ogg",
"data": "<base64 编码>",
"format": "ogg",
"duration": 5
}
```
### 卡片 Schema
卡片由可选的 header 和元素列表组成:
```json
{
"header": {
"title": "卡片标题",
"color": "blue"
},
"elements": [ ... ]
}
```
**支持的颜色:** `blue``green``red``orange``purple``grey``turquoise``violet``indigo``wathet``yellow``carmine`
#### 元素类型
**Markdown 文本**
```json
{"type": "markdown", "content": "**加粗** 和 _斜体_"}
```
**分割线**
```json
{"type": "divider"}
```
**操作按钮行**
```json
{
"type": "actions",
"buttons": [
{"text": "点我", "btn_type": "primary", "value": "cmd:/do-something"}
],
"layout": "row"
}
```
`btn_type``"primary"``"default"``"danger"`
`layout``"row"`(默认)、`"equal_columns"`
**列表项(描述 + 按钮)**
```json
{
"type": "list_item",
"text": "GPT-4 — 最强模型",
"btn_text": "选择",
"btn_type": "primary",
"btn_value": "cmd:/model switch gpt-4"
}
```
**下拉选择器**
```json
{
"type": "select",
"placeholder": "选择一个模型",
"options": [
{"text": "GPT-4", "value": "cmd:/model switch gpt-4"},
{"text": "Claude", "value": "cmd:/model switch claude"}
],
"init_value": "cmd:/model switch gpt-4"
}
```
**脚注**
```json
{
"type": "note",
"text": "提示:使用 /help 查看所有命令",
"tag": "可选的机器标签"
}
```
---
## Session Key 格式
Session key 遵循以下格式:
```
{platform}:{scope}:{user_id}
```
- **platform**:注册时的 `platform` 名称(如 `wechat`)。
- **scope**:分组范围 — 可以是群/频道 ID,也可以与 `user_id` 相同(一对一私聊)。
- **user_id**:用户在平台上的唯一标识。
示例:
- `wechat:user123:user123` — 私聊
- `wechat:group456:user123` — 用户在群聊中
- `matrix:room789:alice` — Matrix 聊天室
适配器负责构建一致的 session key。
---
## 会话管理 REST API
除了用于实时消息的 WebSocket 协议外,Bridge Server 还在同一端口上暴露 HTTP REST 端点用于会话管理。适配器可以通过这些接口列出、创建、切换和删除会话,无需单独配置管理 API。
### 认证
使用与 WebSocket 连接相同的 token
| 方式 | 示例 |
|------|------|
| Header | `Authorization: Bearer your-secret` |
| Query 参数 | `?token=your-secret` |
### 响应格式
所有响应使用统一的信封格式:
```json
{"ok": true, "data": { ... }}
{"ok": false, "error": "错误信息"}
```
### 端点
所有端点相对于 Bridge Server 基础 URL(如 `http://localhost:9810`)。
#### GET /bridge/sessions
列出指定 session key 的所有会话。
**Query 参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `session_key` | string | 是 | 要查询会话的 session key(如 `wechat:user123:user123`)。 |
**响应:**
```json
{
"ok": true,
"data": {
"sessions": [
{
"id": "s1",
"name": "default",
"history_count": 12
},
{
"id": "s2",
"name": "work",
"history_count": 5
}
],
"active_session_id": "s1"
}
}
```
---
#### POST /bridge/sessions
创建新的命名会话。
**请求体:**
```json
{
"session_key": "wechat:user123:user123",
"name": "work"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `session_key` | string | 是 | 用户的 session key。 |
| `name` | string | 否 | 人类可读的会话名称。默认为 `"default"`。 |
**响应:**
```json
{
"ok": true,
"data": {
"id": "s3",
"name": "work",
"message": "session created"
}
}
```
---
#### GET /bridge/sessions/{id}
获取会话详情及消息历史。
**Query 参数:**
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `session_key` | string | (必填) | 用于定位项目上下文的 session key。 |
| `history_limit` | int | 50 | 返回的最大历史条数。 |
**响应:**
```json
{
"ok": true,
"data": {
"id": "s1",
"name": "default",
"history": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!有什么可以帮你的?"}
]
}
}
```
---
#### DELETE /bridge/sessions/{id}
删除会话及其历史记录。
**Query 参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `session_key` | string | 是 | 用于定位项目上下文的 session key。 |
**响应:**
```json
{
"ok": true,
"data": {
"message": "session deleted"
}
}
```
---
#### POST /bridge/sessions/switch
切换指定 session key 的活跃会话。
**请求体:**
```json
{
"session_key": "wechat:user123:user123",
"target": "s2"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `session_key` | string | 是 | Session key。 |
| `target` | string | 是 | 要切换到的会话 ID 或名称。 |
**响应:**
```json
{
"ok": true,
"data": {
"message": "session switched",
"active_session_id": "s2"
}
}
```
---
## 错误处理
### 断线重连
WebSocket 连接断开时,适配器应:
1. 使用指数退避等待(起始 1 秒,最大 60 秒)。
2. 重新连接并发送新的 `register` 消息。
3. 恢复正常运行 — cc-connect 独立于连接维护会话状态。
### 消息顺序
单个 WebSocket 连接内的消息是有序的。cc-connect 按 session key 顺序处理适配器消息。
### 超时
- **Ping 间隔**:适配器应至少每 30 秒发送一次 `ping`
- **连接超时**cc-connect 在 90 秒没有收到 ping 后关闭空闲连接。
- **回复超时**:如果 agent 耗时过长,cc-connect 可能发送错误回复。适配器不需要特殊处理。
---
## 配置示例
```toml
[bridge]
enabled = true
port = 9810
token = "一个强随机密钥"
# 可选:限制哪些适配器可以连接(按平台名称)。
# 默认:允许所有已注册的适配器。
# allow_platforms = ["wechat", "matrix"]
```
不需要为每个适配器单独配置项目 — 适配器默认关联到**默认项目**,或在 `register` 消息中指定 `project` 字段绑定到特定项目。
---
## SDK 开发指南
开发适配器时,请遵循以下原则:
1. **保持无状态** — 适配器应该是一个轻量的协议转换层。所有会话状态存储在 cc-connect 中。
2. **处理断线重连** — 网络故障是正常的,实现指数退避重试。
3. **如实声明能力** — 只声明你的平台实际支持的能力。
4. **忠实使用 `reply_ctx`** — 始终原样回传原始消息中的 `reply_ctx`
5. **二进制数据用 Base64** — 图片、文件和音频通过 base64 编码字符串传输。
6. **记录错误而非崩溃** — 收到未知消息类型时,记录日志并继续运行。
### 最小适配器示例(Python 伪代码)
```python
import asyncio
import json
import websockets
async def main():
uri = "ws://localhost:9810/bridge/ws?token=your-secret"
async with websockets.connect(uri) as ws:
# 1. 注册
await ws.send(json.dumps({
"type": "register",
"platform": "my-chat",
"capabilities": ["text", "buttons"]
}))
ack = json.loads(await ws.recv())
assert ack["ok"], f"注册失败: {ack['error']}"
# 2. 启动消息循环
async def recv_loop():
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "reply":
send_to_chat_platform(msg["reply_ctx"], msg["content"])
elif msg["type"] == "buttons":
send_buttons_to_chat(msg["reply_ctx"], msg["content"], msg["buttons"])
# ... 处理其他类型
async def send_loop():
while True:
chat_msg = await get_next_chat_message()
await ws.send(json.dumps({
"type": "message",
"msg_id": chat_msg.id,
"session_key": f"my-chat:{chat_msg.user_id}:{chat_msg.user_id}",
"user_id": chat_msg.user_id,
"user_name": chat_msg.user_name,
"content": chat_msg.text,
"reply_ctx": chat_msg.conversation_id
}))
await asyncio.gather(recv_loop(), send_loop())
asyncio.run(main())
```
---
## 版本管理
协议版本通过 `register` 消息的 `metadata.protocol_version` 声明。当前版本为 `1`。cc-connect 会拒绝不兼容版本的连接,并在 `register_ack` 中返回错误。
```json
{
"type": "register",
"platform": "my-chat",
"capabilities": ["text"],
"metadata": {
"protocol_version": 1
}
}
```
+21
View File
@@ -0,0 +1,21 @@
# Community Tools
This page lists community-maintained tools and helpers built around cc-connect.
These tools are not official cc-connect components unless explicitly stated. Please report issues, feature requests, and support questions to each tool's own repository.
## CC-Tray
A lightweight Windows tray controller for an already configured `cc-connect daemon` running inside WSL.
- Repository: https://github.com/STAR-REIN/CC-Tray
- Platform: Windows + WSL
- Package: single native exe, about 260 KiB
- Features:
- daemon status/start/restart/stop
- WSL distribution/user detection
- optional keep-running mode
- optional Windows startup
- optional stop-on-exit
- EN/ZH/KO/JA/FR menu language support
- Maintainer: [STAR-REIN](https://github.com/STAR-REIN)
+306
View File
@@ -0,0 +1,306 @@
# 钉钉 (DingTalk) 接入指南
本文档介绍如何将 **cc-connect** 接入钉钉,让你可以通过钉钉机器人远程调用 Claude Code。
## 前置要求
- 钉钉账号(个人或企业均可)
- 一台可运行 cc-connect 的设备(无需公网 IP
- Claude Code 已安装并配置完成
> 💡 **优势**:使用 Stream 模式(WebSocket 长连接),无需公网 IP、无需域名、无需反向代理
---
## 第一步:创建钉钉应用
### 1.1 进入钉钉开放平台
访问 [钉钉开放平台](https://open.dingtalk.com/) 并登录你的钉钉账号。
### 1.2 创建应用
1. 点击「控制台」进入开发者后台
2. 选择「应用开发」→「企业内部开发」(或「H5微应用」)
3. 点击「创建应用」
> 💡 **个人开发者**:钉钉开放平台支持个人开发者创建应用。
### 1.3 填写应用信息
| 字段 | 填写建议 |
|------|---------|
| 应用名称 | `cc-connect` 或你喜欢的名称 |
| 应用描述 | `Claude Code 远程助手` |
| 应用图标 | 上传一个喜欢的图标 |
---
## 第二步:获取凭证
### 2.1 进入应用详情
在应用列表中点击刚创建的应用,进入应用详情页。
### 2.2 获取凭证信息
在「基础信息」页面,你会看到:
```
AppKey: dingxxxxxxxxxxxxxxx
AppSecret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
> ⚠️ **重要**:请妥善保存这两个凭证,后续配置 cc-connect 时需要用到。AppSecret 只会显示一次。
### 2.3 配置到 cc-connect
将凭证配置到 cc-connect 的 `config.toml` 中:
```toml
[[projects]]
name = "my-project"
[projects.agent]
type = "claudecode"
[projects.agent.options]
work_dir = "/path/to/your/project"
mode = "default"
[[projects.platforms]]
type = "dingtalk"
[projects.platforms.options]
client_id = "dingxxxxxxxxxxxxxxx"
client_secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```
---
## 第三步:配置机器人能力
### 3.1 启用机器人
1. 在应用详情页,找到「机器人配置」
2. 点击「启用机器人」
### 3.2 配置机器人信息
| 配置项 | 建议值 |
|-------|--------|
| 机器人名称 | `cc-connect` |
| 机器人描述 | `Claude Code 远程助手` |
| 机器人头像 | 与应用图标一致 |
---
## 第四步:配置权限
### 4.1 进入权限管理
在应用详情页,点击「权限管理」。
### 4.2 申请必要权限
搜索并申请以下权限:
| 权限名称 | 权限标识 | 用途 |
|---------|---------|------|
| 成员信息读权限 | `qyapi_get_member` | 获取用户信息 |
| 企业内消息通知发送 | `qyapi_chat_manage_send` | 发送消息 |
| 机器人消息发送 | `qyapi_robot_message_send` | 机器人发送消息 |
| 读取消息 | `qyapi_get_chat_message` | 读取消息内容 |
### 4.3 申请权限
点击「申请权限」,等待审批通过。
---
## 第五步:配置事件订阅(Stream 模式)
### 5.1 什么是 Stream 模式?
**Stream 模式**是钉钉开放平台提供的一种基于 WebSocket 长连接的集成方式:
| 特性 | 说明 |
|------|------|
| ✅ 无需公网 IP | 内网环境也能接入 |
| ✅ 无需域名 | 不需要配置域名 |
| ✅ 无需 HTTPS | 不需要 SSL 证书 |
| ✅ 自动重连 | 断线后自动恢复 |
| ✅ 简化配置 | 只需集成 SDK |
### 5.2 工作原理
```
┌─────────────────────────────────────────────────────────────┐
│ 钉钉云 │
│ │
│ 用户消息 ──→ 钉钉开放平台 ──→ Stream Gateway │
│ │ │
└──────────────────────────────────────┼───────────────────────┘
│ WebSocket 长连接
│ (无需公网IP)
┌─────────────────────────────────────────────────────────────┐
│ 你的本地环境 │
│ │
│ cc-connect ◄──► Claude Code CLI ◄──► 你的项目代码 │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 5.3 配置 Stream 模式
1. 在应用详情页,找到「事件订阅」
2. 选择「**Stream 模式**」
3. 无需配置回调地址
### 5.4 添加订阅事件
在事件配置中添加以下事件:
| 事件名称 | 事件标识 | 用途 |
|---------|---------|------|
| 机器人消息 | `chat_add_user` | 用户与机器人建立会话 |
| 收到消息 | `chat_add_message` | 收到用户消息 |
### 5.5 保存配置
点击「保存」完成事件订阅配置。
---
## 第六步:启动 cc-connect
### 6.1 启动服务
```bash
cc-connect
# 或指定配置文件
cc-connect -config /path/to/config.toml
```
### 6.2 验证连接
启动后,cc-connect 会自动与钉钉建立 Stream 长连接。你会在日志中看到:
```
level=INFO msg="dingtalk: stream connected" client_id=dingxxxxxxxxxxxxxxx
level=INFO msg="platform started" project=my-project platform=dingtalk
level=INFO msg="cc-connect is running" projects=1
```
---
## 第七步:发布应用
### 7.1 提交审核
1. 在应用详情页,点击「版本管理与发布」
2. 点击「创建版本」
3. 填写版本号和更新说明
4. 点击「申请发布」
### 7.2 等待审核
- **企业内部应用**:通常立即可用
- **企业应用**:需要管理员审批
---
## 第八步:添加机器人到会话
### 8.1 单聊使用
1. 在钉钉中,点击右上角「+」→「添加机器人」
2. 搜索你创建的机器人
3. 添加后即可发送消息
### 8.2 群聊使用
1. 进入目标群聊
2. 点击群设置 → 「群机器人」
3. 添加你创建的机器人
---
## 使用示例
配置完成后,你可以在钉钉中这样使用:
```
用户: 帮我分析一下当前项目的结构
cc-connect: 🤔 思考中...
cc-connect: 🔧 执行: Bash(ls -la)
cc-connect: ✅ 这是一个 Node.js 项目,包含以下目录...
```
---
## Stream 模式 vs Webhook 模式
| 对比项 | Stream 模式 | Webhook 模式 |
|-------|-------------|--------------|
| 公网 IP | ❌ 不需要 | ✅ 需要 |
| 域名 | ❌ 不需要 | ✅ 需要 |
| HTTPS 证书 | ❌ 不需要 | ✅ 需要 |
| 反向代理 | ❌ 不需要 | ✅ 需要 |
| 配置复杂度 | 简单 | 较复杂 |
| 连接方式 | WebSocket | HTTP 回调 |
| 适用场景 | 本地开发、内网 | 生产环境 |
---
## 常见问题
### Q: Stream 模式和 Webhook 模式如何选择?
- **开发/测试环境**:推荐 Stream 模式,无需公网资源
- **生产环境**:两者都可以,Stream 模式配置更简单
### Q: 长连接断开怎么办?
cc-connect 内置了自动重连机制,断开后会自动尝试重新连接。
### Q: 消息发送后没有响应?
检查以下项目:
1. cc-connect 服务是否正常运行
2. Stream 连接是否建立成功(查看日志)
3. 事件订阅是否配置正确
### Q: 提示权限不足?
确保已在「权限管理」中申请并获得了所有必要权限。
### Q: 如何调试?
使用钉钉开放平台的「调试工具」进行测试。
---
## 参考链接
- [钉钉开放平台](https://open.dingtalk.com/)
- [钉钉开放平台文档](https://open.dingtalk.com/document/)
- [Stream 模式介绍](https://open.dingtalk.com/document/development/introduction-to-stream-mode)
- [Stream 模式协议接入说明](https://open.dingtalk.com/document/direction/stream-mode-protocol-access-description)
- [机器人开发指南](https://open.dingtalk.com/document/org/robot-message-subscription)
- [Spring Boot Stream 模式教程](https://m.blog.csdn.net/andrew_dear/article/details/140853791)
- [Python Stream 模式开发指南](https://m.blog.csdn.net/gitblog_00219/article/details/155120234)
---
## 下一步
- [接入飞书](./feishu.md)
- [接入微博](./weibo.md)
- [接入 Telegram](./telegram.md)
- [接入 Slack](./slack.md)
- [接入 Discord](./discord.md)
- [返回首页](../README.md)
+309
View File
@@ -0,0 +1,309 @@
# Discord Setup Guide
This guide walks you through connecting **cc-connect** to Discord, so you can chat with your local Claude Code via a Discord bot.
## Prerequisites
- A Discord account
- A machine that can run cc-connect (no public IP needed)
- Claude Code installed and configured
> 💡 **Advantage**: Uses Gateway (WebSocket) — no public IP, no domain, no reverse proxy needed.
---
## Step 1: Create a Discord Application
### 1.1 Open the Developer Portal
Go to [Discord Developer Portal](https://discord.com/developers/applications) and sign in.
### 1.2 Create a New Application
1. Click "New Application" in the top right
2. Enter an application name (e.g. `cc-connect`)
3. Agree to the Terms of Service
4. Click "Create"
---
## Step 2: Create a Bot User
### 2.1 Go to Bot Settings
In the left sidebar, click "Bot".
### 2.2 Add a Bot
1. Click "Add Bot"
2. Confirm the action
### 2.3 Configure Bot Info
| Field | Suggested Value |
|-------|----------------|
| Username | `cc-connect` |
| Avatar | Upload an icon you like |
---
## Step 3: Get the Bot Token
### 3.1 Generate Token
On the Bot page:
1. Click "Reset Token"
2. You may need to enter a 2FA code
3. Click "Copy" to copy the token
> ⚠️ The token is only shown once — save it immediately! Format: `MTk4NjIyNDgzNDcOTY3NDUxMg.G8vKqh.xxx...`
### 3.2 Lost Your Token?
Click "Reset Token" at any time to regenerate. The old token will be invalidated immediately.
---
## Step 4: Configure Privileged Intents (Important!)
### 4.1 What Are Intents?
Intents control which events your bot can receive from Discord's Gateway.
### 4.2 Enable Required Intents
On the Bot page, under "Privileged Gateway Intents", enable:
| Intent | Purpose | Required? |
|--------|---------|-----------|
| **Message Content Intent** | Read message content | ✅ **Required** |
| Presence Intent | Read user status | Optional |
| Server Members Intent | Read server members | Optional |
> ⚠️ **You must enable Message Content Intent**, or the bot won't be able to read messages!
### 4.3 Save Changes
Click "Save Changes".
---
## Step 5: Configure cc-connect
Add the token to your `config.toml`:
```toml
[[projects]]
name = "my-project"
[projects.agent]
type = "claudecode"
[projects.agent.options]
work_dir = "/path/to/your/project"
mode = "default"
[[projects.platforms]]
type = "discord"
[projects.platforms.options]
token = "MTk4NjIyNDgzNDcOTY3NDUxMg.G8vKqh.xxx..."
# thread_isolation = true # Optional: isolate each agent session in its own Discord thread
# progress_style = "legacy" # Optional: legacy | compact | card
```
> cc-connect automatically configures the required Intents (MESSAGE_CONTENT, GUILD_MESSAGES, DIRECT_MESSAGES).
> With `thread_isolation = true`, cc-connect creates or reuses a Discord thread for each session and routes follow-up messages by thread channel ID.
> `progress_style = "compact"` merges thinking/tool updates into one editable message; `progress_style = "card"` renders a Discord-native embed progress card and still sends the final answer as a normal message.
---
## Step 6: Generate an Invite Link
### 6.1 Go to OAuth2 Settings
In the left sidebar, click "OAuth2" → "URL Generator".
### 6.2 Select Scopes
Under "Scopes", check:
-`bot`
### 6.3 Select Permissions
Under "Bot Permissions", check:
| Permission | Purpose |
|------------|---------|
| Read Messages/View Channels | Read messages |
| Send Messages | Send messages |
| Create Public Threads | Create a new thread for a fresh agent session |
| Send Messages in Threads | Send messages in threads |
| Read Message History | Read message history |
### 6.4 Copy the Link
1. The invite link will be generated at the bottom of the page
2. Click "Copy"
---
## Step 7: Invite the Bot to Your Server
### 7.1 Open the Invite Link
Open the copied URL in your browser and sign in to Discord.
### 7.2 Select a Server
Choose the server you want to add the bot to from the dropdown.
### 7.3 Authorize
Review the permissions and click "Authorize". Complete the CAPTCHA if prompted.
---
## Step 8: Start cc-connect
### 8.1 Launch
```bash
cc-connect
# Or specify a config file
cc-connect -config /path/to/config.toml
```
### 8.2 Verify Connection
You should see logs like:
```
level=INFO msg="discord: connected" bot=cc-connect#0000
level=INFO msg="platform started" project=my-project platform=discord
level=INFO msg="cc-connect is running" projects=1
```
---
## Step 9: Start Chatting
### 9.1 Channel Usage
Send a message in any channel where the bot has permissions.
### 9.2 Direct Message
1. Click the bot's avatar
2. Send a DM
---
## Usage Example
```
User: Help me analyze the current project structure
cc-connect: 🤔 Thinking...
cc-connect: 🔧 Tool: Bash(ls -la)
cc-connect: Here's the project structure...
```
If you enable `progress_style = "card"`, Discord shows one editable progress embed during the turn, then the final answer arrives as a separate normal message. This reduces channel noise compared with the legacy multi-message flow.
---
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Discord Cloud │
│ │
│ User Message ──→ Discord Gateway ◄── WebSocket │
│ │ │
└─────────────────────────┼────────────────────────────────────┘
│ WebSocket (no public IP needed)
┌─────────────────────────────────────────────────────────────┐
│ Your Local Machine │
│ │
│ cc-connect ◄──► Claude Code CLI ◄──► Your Project Code │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Discord Gateway Features
| Feature | Details |
|---------|---------|
| **Connection** | WebSocket |
| **Public IP** | ❌ Not needed |
| **Heartbeat** | Automatic keepalive |
| **Reconnection** | Automatic on disconnect |
| **Intents** | Must declare required event types |
| **Message limit** | 2000 characters per message (auto-split by cc-connect) |
| **Markdown** | Full native support |
---
## FAQ
### Q: Bot can't read message content?
**Most common issue**: Message Content Intent is not enabled!
Fix:
1. Go to Discord Developer Portal
2. Select your app → Bot
3. Enable "Message Content Intent"
4. Save changes
5. Restart cc-connect
### Q: Bot connects then immediately disconnects?
Check:
1. Is the bot token correct?
2. Are intents configured properly?
3. Are you hitting Discord rate limits? (from frequent reconnects)
### Q: Bot doesn't appear in the server?
1. Make sure you used the invite link to add the bot
2. Check if the bot was kicked from the server
### Q: How to regenerate the token?
1. Go to Discord Developer Portal
2. Select your app → Bot
3. Click "Reset Token"
4. Update your config.toml
### Q: Bot has insufficient permissions?
1. Generate a new invite link with the correct permissions
2. Re-invite the bot to the server
---
## References
- [Discord Developer Portal](https://discord.com/developers/applications)
- [Discord API Documentation](https://discord.com/developers/docs/intro)
- [Bot Getting Started Guide](https://discord.com/developers/docs/getting-started)
- [Gateway Intents](https://discord.com/developers/docs/topics/gateway#privileged-intents)
- [OAuth2 Scopes](https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes)
---
## See Also
- [Feishu Setup](./feishu.md)
- [DingTalk Setup](./dingtalk.md)
- [Weibo Setup](./weibo.md)
- [Telegram Setup](./telegram.md)
- [Slack Setup](./slack.md)
- [Back to README](../README.md)
+443
View File
@@ -0,0 +1,443 @@
# 飞书 (Feishu/Lark) 接入指南
本文档介绍如何将 **cc-connect** 接入飞书,让你可以通过飞书机器人远程调用 Claude Code。
## 前置要求
- 飞书账号(个人或企业均可)
- 一台可运行 cc-connect 的设备(无需公网 IP
- Claude Code 已安装并配置完成
> 💡 **优势**:使用长连接模式,无需公网 IP、无需域名、无需反向代理(ngrok/frp)
---
## 快速配置(推荐)
如果你已经装好 `cc-connect`,可以直接用内置命令完成“新建机器人/关联已有机器人”,并自动写回 `config.toml`
```bash
# 推荐:统一入口
cc-connect feishu setup --project my-project
cc-connect feishu setup --project my-project --app cli_xxx:sec_xxx
# 强制模式(一般不需要)
cc-connect feishu new --project my-project
cc-connect feishu bind --project my-project --app cli_xxx:sec_xxx
```
三者区别:
| 命令 | 作用 | 何时用 |
|------|------|--------|
| `setup` | 统一入口:无凭证走 `new`,有凭证走 `bind` | **默认就用这个** |
| `new` | 强制二维码新建(不接受 `--app` | 明确要重走扫码新建 |
| `bind` | 强制关联已有凭证(必须 `app_id/app_secret` | 明确只做凭证关联 |
补充:
- `setup --app ...``bind --app ...` 功能等价。
- `setup/new` 会在终端打印二维码和 URL,使用飞书/Lark 手机 App 扫码完成创建。
- `--project` 不存在时会自动创建该项目;若项目存在但没有 `feishu/lark` 平台,也会自动补一个。
- 写回配置时仅定点更新目标字段(`app_id``app_secret``allow_from` 等),尽量保留原有注释与排版。
- 该流程会回填凭证;通过扫码新建时,飞书通常会同时预配权限与事件订阅。
- 仍建议在开放平台核验:应用已发布、权限状态正常、可用范围符合预期。
---
## 第一步:创建飞书企业自建应用
### 1.1 进入飞书开放平台
访问 [飞书开放平台](https://open.feishu.cn/) 并登录你的飞书账号。
### 1.2 创建应用
1. 点击右上角「控制台」进入开发者后台
2. 点击「创建企业自建应用」
> 💡 **个人用户也可以创建**:飞书开放平台支持个人开发者创建应用,无需企业认证。
### 1.3 填写应用信息
| 字段 | 填写建议 |
|------|---------|
| 应用名称 | `cc-connect` 或你喜欢的名称 |
| 应用描述 | `Claude Code 远程助手` |
| 应用图标 | 上传一个喜欢的图标 |
---
## 第二步:获取凭证
### 2.1 进入凭据页面
在应用详情页,左侧导航栏点击 **「凭据与基础信息」**。
### 2.2 获取 App ID 和 App Secret
你会看到以下信息:
```
App ID: cli_axxxxxxxxxxxx
App Secret: QhkMpxxxxxxxxxxxxxxxxxxxx
```
> ⚠️ **重要**:请妥善保存这两个凭证,后续配置 cc-connect 时需要用到。App Secret 只会显示一次,如果忘记了需要重置。
### 2.3 配置到 cc-connect
将凭证配置到 cc-connect 的 `config.toml` 中:
```toml
[[projects]]
name = "my-project"
[projects.agent]
type = "claudecode"
[projects.agent.options]
work_dir = "/path/to/your/project"
mode = "default"
[[projects.platforms]]
type = "feishu"
[projects.platforms.options]
app_id = "cli_axxxxxxxxxxxx"
app_secret = "QhkMpxxxxxxxxxxxxxxxxxxxx"
# domain = "https://open.feishu.cn" # 可选:覆盖运行时 API/WebSocket 域名
# enable_feishu_card = true # 可选:关闭后统一回退纯文本回复
# thread_isolation = true # 可选:按飞书 thread/root 隔离群聊会话
# progress_style = "legacy" # 可选:legacy | compact | card
# done_emoji = "none" # 可选:agent 完成回复后添加的表情回复(如 "Done");设为 "none" 可禁用
```
> 如果应用没有交互卡片权限,或后台未配置卡片回调,可将 `enable_feishu_card = false`,让所有命令统一走纯文本回复,避免卡片发送失败后用户看不到内容。
> 如果开启 `thread_isolation = true`,群聊里每个根消息 / reply thread 会对应一个独立 agent session;私聊行为保持原样。
> `progress_style = "compact"` 会把思考/工具进度合并到一条可更新消息里,减少刷屏;`legacy` 保持原有逐条发送;`card` 会使用结构化卡片(标题 + 进度块)持续更新同一条消息,观感比纯文本更清晰。
> `domain` 只影响运行时 API / WebSocket 请求地址;CLI `setup/new/bind` 的引导域名仍然使用内置默认值。
> `done_emoji` 设置后,agent 每次完成回复时会在用户消息上添加指定表情(如 `"Done"` → ✅)。先移除 "OnIt" 表情(如果有),再添加 done 表情。在 quiet 模式下特别有用,因为飞书卡片原地更新不触发推送,done 表情可以通知用户 agent 已完成。设为 `"none"` 或不配置则禁用。
---
## 第三步:配置应用能力
### 3.1 启用机器人能力
1. 左侧导航栏点击 **「应用能力」** → **「机器人」**
2. 点击「启用机器人」
### 3.2 配置机器人信息
| 配置项 | 建议值 |
|-------|--------|
| 机器人名称 | `cc-connect` |
| 机器人描述 | `Claude Code 远程助手` |
| 机器人头像 | 与应用图标一致 |
---
## 第四步:配置权限
### 4.1 进入权限管理
左侧导航栏点击 **「权限管理」**。
### 4.2 申请必要权限
在「权限配置」中搜索并添加以下权限:
| 权限名称 | 权限标识 | 用途 |
|---------|---------|------|
| 获取与更新用户基本信息 | `contact:user.base:readonly` | 获取用户信息 |
| 获取群组中用户@机器人消息 | `im:message.group_at_msg:readonly` | 接收群消息 |
| 读取用户发给机器人的单聊消息 | `im:message.p2p_msg:readonly` | 接收私聊消息 |
| 获取群组中所有消息(敏感权限) | `im:message.group_msg` | 读取群消息内容 |
| 读取单聊消息 | `im:message.p2p_msg:readonly` | 读取私聊内容 |
| 以应用身份发送群消息 | `im:message:send_as_bot` | 发送消息回复用户 |
### 4.3 发布权限申请
配置完权限后,点击「申请发布」使权限生效。
---
## 第五步:配置事件与回调订阅(长连接模式)
### 5.1 进入事件与回调页面
左侧导航栏点击 **「事件与回调」**。
### 5.2 选择事件配置
在标签页中点击: **「事件配置」**。
在「订阅方式」中选择:
```
✅ 使用长连接接收事件
```
点击**保存**。
点击**添加事件**。
在事件配置中添加以下事件:
| 事件名称 | 事件标识 | 用途 |
|---------|---------|------|
| 接收消息 | `im.message.receive_v1` | 接收用户发送的消息 |
### 5.3 选择回调配置
在标签页中点击: **「回调配置」**。
在「订阅方式」中选择:
```
✅ 使用长连接接收事件
```
点击**保存**。
点击**添加回调**。
在回调配置中添加以下回调:
| 回调名称 | 回调标识 | 用途 |
|---------|---------|------|
| 卡片回调 | `card.action.trigger` | 响应交互卡片按钮点击(权限确认、provider 切换等) |
> ⚠️ **重要**:如果不订阅 `card.action.trigger` 回调,用户点击卡片上的按钮(如权限确认、provider 选择等)时将无法正常响应,飞书客户端可能会显示加载超时或错误提示。如果暂时无法添加该回调,可以在配置中设置 `enable_feishu_card = false` 关闭交互卡片功能,所有交互将回退到纯文本模式。
### 5.4 创建版本
点击 **「创建版本」** 发布新版本以应用事件与回调配置。
---
## 第六步:启动 cc-connect
### 6.1 启动服务
```bash
cc-connect
# 或指定配置文件
cc-connect -config /path/to/config.toml
```
### 6.2 验证连接
启动后,cc-connect 会自动与飞书建立 WebSocket 长连接。你会在日志中看到:
```
level=INFO msg="platform started" project=my-project platform=feishu
level=INFO msg="cc-connect is running" projects=1
[Info] connected to wss://msg-frontier.feishu.cn/ws/v2?...
```
---
## 第七步:发布应用
### 7.1 提交审核
1. 左侧导航栏点击 **「版本管理与发布」**
2. 点击「创建版本」
3. 填写版本号和更新说明
4. 点击「保存并发布」
### 7.2 可用性设置
- **企业版**:发布后需要管理员审批才能使用
- **个人版**:发布后立即可用
---
## 第八步:添加机器人到会话
### 8.1 单聊使用
在飞书中搜索你的机器人名称,直接发送消息即可开始对话。
### 8.2 群聊使用
1. 进入目标群聊
2. 点击群设置 → 「群机器人」
3. 添加你创建的机器人
---
## 使用示例
配置完成后,你可以在飞书中这样使用:
```
用户: 帮我分析一下当前项目的结构
cc-connect: 🤔 思考中...
cc-connect: 🔧 执行: Bash(ls -la)
cc-connect: ✅ 这是一个 Node.js 项目,包含以下目录...
```
---
## 架构图
```
┌─────────────────────────────────────────────────────────────┐
│ 飞书云 │
│ │
│ 用户消息 ──→ 飞书开放平台 ──→ WebSocket Gateway │
│ │ │
└──────────────────────────────────────┼───────────────────────┘
│ WebSocket 长连接
│ (无需公网IP)
┌─────────────────────────────────────────────────────────────┐
│ 你的本地环境 │
│ │
│ cc-connect ◄──► Claude Code CLI ◄──► 你的项目代码 │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Mention 功能
开启 `resolve_mentions = true` 后,机器人发出的消息中 `@显示名` 会自动替换为飞书原生 at 标签。
### 配置
```toml
[projects.platforms.options]
resolve_mentions = true
```
### 语法
直接使用 `@显示名`,无需特殊标记:
```
@张三 请查看巡检报告
```
### 使用示例
**Cron 定时任务:**
```bash
cc-connect cron add \
--cron "0 9 * * *" \
--prompt "执行每日巡检报告,完成后通知 @张三 和 @李四 查看" \
--desc "每日巡检"
```
**AI 对话中:**
AI 输出中包含 `@某人` 时,发送到飞书前会自动匹配并替换。
### 工作原理
1. 开启 `resolve_mentions` 后,发送消息前拉取群成员列表(懒加载,首次才拉)
2. 成员列表缓存 1 小时,减少 API 调用
3. 按名字长度从长到短匹配(`@张三丰` 优先于 `@张三`),避免部分匹配
4. 未匹配到的 `@xxx` 保留原文不处理
5. 根据消息类型自动选择正确的飞书 at 语法(文本消息 vs 卡片消息)
### 权限要求
需要以下飞书应用权限之一:
- `im:chat`(获取与更新群组信息)
- `im:chat:readonly`(获取群组信息)
- `im:chat.members:read`(查看群成员)
### 注意事项
- 名字匹配为精确匹配(`@张三` 只匹配显示名恰好是「张三」的成员)
- 同名成员取第一个匹配到的
- 被 at 的人必须是当前群的成员
- 未开启 `resolve_mentions` 时不会触发任何成员查询
---
## 常见问题
### Q: 长连接和 Webhook 有什么区别?
| 对比项 | 长连接模式 | Webhook 模式 |
|-------|-----------|-------------|
| 公网 IP | ❌ 不需要 | ✅ 需要 |
| 域名 | ❌ 不需要 | ✅ 需要 |
| HTTPS 证书 | ❌ 不需要 | ✅ 需要 |
| 反向代理 | ❌ 不需要 | ✅ 需要(ngrok/frp |
| 配置复杂度 | 简单 | 较复杂 |
| 适用场景 | 本地开发、内网 | 生产环境 |
### Q: 长连接断开怎么办?
cc-connect 内置了自动重连机制,断开后会自动尝试重新连接。
### Q: 消息发送后没有响应?
检查以下项目:
1. cc-connect 服务是否正常运行
2. 长连接是否建立成功(查看日志)
3. 事件订阅是否配置了 `im.message.receive_v1`
### Q: 点击卡片按钮没有反应或报错?
cc-connect 默认使用交互卡片显示权限确认、provider 选择等操作。如果点击按钮后无响应、显示加载超时或报错,请检查:
1. **事件订阅**:确认已在飞书开放平台订阅了 `card.action.trigger` 事件(详见第五步)
2. **应用发布**:修改事件订阅后需要重新发布应用版本
3. **权限配置**:确保应用有 `im:message:send_as_bot` 权限
**快速解决方案**:如果暂时无法配置卡片回调,可以在 `config.toml` 中关闭交互卡片:
```toml
[projects.platforms.options]
enable_feishu_card = false
```
关闭后,所有交互将回退为纯文本模式,权限确认等操作通过直接回复文字完成。
### Q: 提示权限不足?
确保已在「权限管理」中申请并获得了所有必要权限,并发布了新版本。
### Q: 扫码页显示 OpenClaw 文案,是不是配置错了?
通常是飞书注册模板侧的展示文案,不影响返回 `app_id/app_secret` 和接入 cc-connect。
### Q: 如何调试消息?
在飞书开放平台「开发调试」→「调试工具」中可以模拟发送消息进行测试。
---
## 参考链接
- [飞书开放平台](https://open.feishu.cn/)
- [飞书开放平台文档](https://open.feishu.cn/document/)
- [机器人开发指南](https://open.feishu.cn/document/ukTMukTMukTM/uYjNwUjL2YDM14iN2ATN)
- [事件订阅文档](https://open.feishu.cn/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM)
- [权限列表](https://open.feishu.cn/document/server-docs/application-scope/scope-list)
- [OpenClaw 飞书接入教程](https://bytedance.larkoffice.com/docx/MFK7dDFLFoVlOGxWCv5cTXKmnMh)
- [飞书 WebSocket 长连接模式](https://m.blog.csdn.net/u014177256/article/details/158267848)
---
## 下一步
- [接入钉钉](./dingtalk.md)
- [接入微博](./weibo.md)
- [接入 Telegram](./telegram.md)
- [接入 Slack](./slack.md)
- [接入 Discord](./discord.md)
- [返回首页](../README.md)
Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 200">
<defs>
<linearGradient id="textGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#0ea5e9;stop-opacity:1" />
<stop offset="100%" style="stop-color:#0284c7;stop-opacity:1" />
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="1.5" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Background -->
<rect width="800" height="200" fill="#0f172a"/>
<!-- Title -->
<text x="400" y="115" font-family="system-ui, -apple-system, sans-serif" font-size="72" fill="url(#textGrad)" text-anchor="middle" font-weight="bold" filter="url(#glow)" letter-spacing="-2">CC-Connect</text>
<!-- Subtitle -->
<text x="400" y="150" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#94a3b8" text-anchor="middle">Bridge AI Agents to Chat Platforms</text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

+18
View File
@@ -0,0 +1,18 @@
# Sponsors Images
This directory contains sponsor logos for the README sponsor section.
## Naming Convention
- Use lowercase: `sponsor-name.png` or `sponsor-name.jpg`
- Recommended size: 150x50 px (logo), 150x150 px (square)
## Adding a Sponsor
1. Add logo image to this directory
2. Update README.md and README.zh-CN.md sponsor table
3. Include sponsor's affiliate link and discount details
## Placeholder
Replace `your-logo-here.png` with actual sponsor logos.
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="60" viewBox="0 0 150 60">
<rect width="150" height="60" fill="#f0f0f0" rx="4"/>
<text x="75" y="35" font-family="Arial, sans-serif" font-size="12" fill="#999" text-anchor="middle">Your Logo Here</text>
</svg>

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+255
View File
@@ -0,0 +1,255 @@
# MAX bot deployment guide
The MAX platform adapter (`platform/max`) supports two delivery modes:
- **Long-poll** (default) — bot pulls updates from `platform-api.max.ru/updates`. Works behind NAT, no public URL needed. From 2026-05-11 MAX throttles long-poll to 2 RPS, so this is best for personal/low-traffic bots.
- **Webhook** — MAX pushes each update to your HTTPS endpoint. Recommended for production; required if you need >2 RPS sustained.
This guide covers the three real-world topologies and a copy-paste config for each.
## Topology A — VPS with public IP and reverse proxy (recommended)
The bot runs on a server that has a public domain and TLS-terminating reverse proxy (nginx, Caddy, Traefik) in front.
```
┌─────────── VPS (one host) ────────────┐
user → MAX cloud ─── HTTPS POST ───▶ │ nginx :443 (TLS) │
https://your.tld │ └ proxy_pass → 127.0.0.1:8090 │
/webhook │ │
│ cc-connect (HTTP :8090, localhost) │
└───────────────────────────────────────┘
```
### Bot config
```toml
[[projects.platforms]]
type = "max"
[projects.platforms.options]
token = "your-max-bot-token"
allow_from = "12345678"
webhook_url = "https://bot.example.com/webhook"
webhook_listen = "127.0.0.1:8090" # bind to loopback only — nginx is the public face
webhook_secret = "long-random-string-here" # optional; recommended
```
### nginx site (`/etc/nginx/sites-available/bot.example.com`)
```nginx
server {
listen 443 ssl;
server_name bot.example.com;
ssl_certificate /etc/letsencrypt/live/bot.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/bot.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
location /webhook {
proxy_pass http://127.0.0.1:8090;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 30s;
proxy_connect_timeout 5s;
client_max_body_size 50M;
}
location / {
default_type text/plain;
return 200 "ok\n";
}
}
server {
listen 80;
server_name bot.example.com;
return 301 https://$host$request_uri;
}
```
Get the cert with `certbot --nginx -d bot.example.com`, then `nginx -t && systemctl reload nginx`.
### Caddy alternative (single file, auto-TLS)
```caddy
bot.example.com {
handle /webhook {
reverse_proxy 127.0.0.1:8090
}
respond / "ok" 200
}
```
That's the entire `Caddyfile`. Caddy obtains and renews the certificate automatically.
## Topology B — Home server + cheap VPS as proxy (current author's setup)
The bot runs at home (no public IP) and a small VPS forwards traffic to it via SSH reverse-tunnel.
```
┌─── VPS ───┐ ┌──── Home ────┐
user → MAX cloud ─── HTTPS ─────────▶ │ nginx │ ──SSH──▶│ cc-connect │
/webhook │ :443→:8090│ -R │ :8090 │
└───────────┘ tunnel └──────────────┘
```
### Bot config (on the home machine)
Same as Topology A — bind to `:8090` (or `127.0.0.1:8090`), set `webhook_url` to the public URL on the VPS:
```toml
webhook_url = "https://bot.example.com/webhook"
webhook_listen = "127.0.0.1:8090"
webhook_secret = "long-random-string-here"
```
### SSH reverse tunnel (from home to VPS)
Add a systemd-user unit, e.g. `~/.config/systemd/user/max-tunnel.service`:
```ini
[Unit]
Description=SSH reverse tunnel for MAX webhook
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/ssh -N \
-R 127.0.0.1:8090:127.0.0.1:8090 \
-p 22 -i %h/.ssh/tunnel_key \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-o StrictHostKeyChecking=accept-new \
tunnel@vps.example.com
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target
```
Enable: `systemctl --user enable --now max-tunnel`.
The tunnel binds `127.0.0.1:8090` on the VPS to the home machine's `:8090`. nginx (Topology A config) then proxies to that loopback address.
### Why a tunnel and not just opening the home firewall
- No need for a static IP at home.
- No port-forwarding on the home router.
- Works the same way from any home network (laptop, mobile hotspot).
- TLS still terminates on the VPS — your home machine never speaks TLS to the internet.
## Topology C — Long-poll (no public URL at all)
Simplest deployment: the bot polls MAX. No reverse proxy, no tunnel, no domain.
```toml
[[projects.platforms]]
type = "max"
[projects.platforms.options]
token = "your-max-bot-token"
allow_from = "12345678"
# webhook_* fields omitted → long-poll mode
```
Use this for personal bots, development, or behind restrictive corporate networks. Not recommended once MAX's 2 RPS long-poll throttle takes effect for higher-traffic bots.
## Configuration reference
| Field | Required | Default | Purpose |
|---|---|---|---|
| `token` | yes | — | Bot token from MAX bot creator |
| `allow_from` | no | `*` (all) | Comma-separated user IDs allowed to message the bot. `*` or empty = no restriction. **Always set this in production** |
| `api_base` | no | `https://platform-api.max.ru` | Override for MAX API base URL (rarely needed) |
| `webhook_url` | no | (empty → long-poll) | Public HTTPS URL MAX will POST updates to. Setting this enables webhook mode |
| `webhook_listen` | no | `:8080` | TCP address the bot binds for incoming webhooks. Use `127.0.0.1:PORT` to restrict to loopback (recommended when behind a reverse proxy) |
| `webhook_path` | no | `/webhook` | Path component the bot serves. Must match the path in `webhook_url`. Lets you host multiple bots on one domain (e.g. `/bot1`, `/bot2`) |
| `webhook_secret` | no | (empty → no check) | Shared secret. If set, requests must include it as `X-Webhook-Secret` header **or** `?s=` query parameter. Mismatch returns 401 |
## Securing the webhook
The MAX public bot API does not currently sign webhook deliveries. Anyone who learns your `webhook_url` can POST garbage to it. Layered defenses:
1. **`webhook_secret`** — set a long random value and embed it in `webhook_url` itself, e.g. `https://bot.example.com/webhook?s=<secret>`. The bot verifies it on every request and rejects mismatches. Keep the secret out of the public URL when possible (use a header instead — see below).
2. **`allow_from`** — restricts which MAX user IDs the bot will respond to. Even if a stranger reaches the webhook, they can't make the bot do anything.
3. **Reverse proxy** — terminate TLS, rate-limit, log. Keep the bot bound to `127.0.0.1` so the only way in is through the proxy.
### Passing the secret as a header instead of a query parameter
If you control the proxy in front of the bot, you can keep the secret out of URLs and access logs:
```nginx
location /webhook {
proxy_pass http://127.0.0.1:8090;
proxy_set_header X-Webhook-Secret "long-random-string-here";
# ...
}
```
Then in the bot's config set `webhook_url = "https://bot.example.com/webhook"` (no query string) and `webhook_secret = "long-random-string-here"`. MAX → nginx adds the header → bot verifies. The secret never appears in URLs MAX or upstream logs see.
## Switching between modes
The bot decides which mode to use purely from config — no rebuild.
### Long-poll → webhook
1. Set `webhook_url`, `webhook_listen` (and optional `webhook_path`, `webhook_secret`) in `config.toml`.
2. Make sure the public URL is reachable and TLS works.
3. `systemctl restart cc-connect` (or however you run it).
On startup the bot calls `POST /subscriptions` against MAX with the new URL. MAX immediately stops delivering long-poll updates and starts pushing.
### Webhook → long-poll
1. Comment out / remove `webhook_url` (and the other `webhook_*` fields) in `config.toml`.
2. Restart the bot.
When the bot stops, it makes a best-effort `DELETE /subscriptions?url=...` to remove the registration. If that call fails (network down, etc.), MAX may keep delivering to the old URL. To force-clear:
```bash
curl -X DELETE \
"https://platform-api.max.ru/subscriptions?url=$(printf %s "$URL" | jq -sRr @uri)&access_token=$TOKEN"
```
After that, restart the bot in long-poll mode.
## Troubleshooting
### `502 Bad Gateway` from nginx when MAX hits the webhook
The bot is not listening on `webhook_listen`. Check, in order:
1. `systemctl --user status cc-connect` — is it running?
2. `ss -tlnp | grep 8090` (or your port) — is something bound?
3. Bot logs — look for `max: webhook listening addr=...` and `max: webhook subscribed url=...`. If you see `connected` but neither of those, you have a startup hang.
### Bot logs `max: connected` but nothing after
Stuck during `Start()`. Common causes:
- `subscribe` HTTP call is timing out — check `platform-api.max.ru` reachability and TLS.
- A mutex deadlock — file a bug.
### Webhook returns 401
Either the secret is wrong, or the request isn't bringing it. Check:
- Header `X-Webhook-Secret` matches `webhook_secret` exactly, OR
- Query param `?s=...` matches.
- If you went via nginx's `proxy_set_header`, verify nginx is actually adding the header (`curl -v` from another box).
### MAX still hits the old webhook after you removed it from config
`Stop()` does best-effort unsubscribe but does not retry on failure. Manually delete the subscription with the `curl -X DELETE` command above, or call `GET /subscriptions?access_token=...` to see what's currently registered.
### How to verify what MAX has registered
```bash
curl "https://platform-api.max.ru/subscriptions?access_token=$TOKEN" | jq
```
Returns the active webhook URL(s) for the bot. Should be at most one.
+66
View File
@@ -0,0 +1,66 @@
# Delete Batch Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add explicit batch deletion support for `/delete 1,2,3`, `/delete 3-7`, and `/delete 1,3-5,8` without introducing ambiguous whitespace-based parsing.
**Architecture:** Keep the existing single-delete flow intact for one plain argument, and add a narrow parser that only activates when `/delete` receives one argument containing comma/range syntax. Resolve list positions from a single session snapshot, deduplicate targets, then execute deletions with a combined reply that reports successes and blocked items.
**Tech Stack:** Go 1.24, existing `core.Engine` command handlers, `testing` package.
---
### Task 1: Add failing command tests
**Files:**
- Modify: `core/engine_test.go`
**Step 1: Write the failing test**
Add command-level tests for:
- `/delete 1,2,3`
- `/delete 3-7`
- `/delete 1,3-5,8`
- invalid explicit syntax like `/delete 1,3-a,8`
- non-supported whitespace-separated args staying non-batch
**Step 2: Run test to verify it fails**
Run: `go test ./core -run TestCmdDelete`
Expected: FAIL because batch parsing does not exist yet.
### Task 2: Implement explicit batch delete parsing
**Files:**
- Modify: `core/engine.go`
- Modify: `core/i18n.go`
**Step 1: Write minimal implementation**
Add a helper that:
- only recognizes one-argument explicit batch syntax
- parses comma-separated integers and inclusive ranges
- rejects malformed items
- deduplicates indices while preserving order
Update `cmdDelete` to:
- route explicit batch syntax through the new helper
- keep existing single-delete behavior for plain one-argument input
- reject ambiguous multi-argument inputs with usage text
- aggregate batch results into one reply
**Step 2: Run targeted tests**
Run: `go test ./core -run TestCmdDelete`
Expected: PASS
### Task 3: Verify no regression in core tests
**Files:**
- Test: `core/engine_test.go`
- Test: `core/i18n_test.go`
**Step 1: Run broader verification**
Run: `go test ./core/...`
Expected: PASS
@@ -0,0 +1,66 @@
# Feishu Delete Card Design
**Date:** 2026-03-11
**Goal:** Let Feishu users enter `/delete` with no arguments to open a card-based multi-select delete flow, while keeping explicit delete arguments such as `/delete 1,2,3` working as direct command execution.
## Scope
- Only `/delete` with no arguments activates card selection mode.
- The normal `/list` card remains unchanged.
- Explicit delete arguments continue to bypass the card flow.
- Card flow is only for card-capable platforms; non-card platforms keep usage text.
## Interaction Flow
1. User sends `/delete`.
2. If the platform supports cards, cc-connect renders a delete-mode session list card.
3. Each session row exposes a single right-side button that toggles selected/unselected state.
4. The card footer provides:
- `删除已选`
- `取消`
- pagination controls
5. `删除已选` opens a confirmation card listing the selected sessions.
6. The confirmation card provides:
- `确认删除`
- `返回继续选择`
7. On confirmation, cc-connect deletes the selected sessions, skips the active session, clears the temporary selection state, and renders a result card.
## State Model
Delete-mode state should be tracked per `sessionKey`, separate from the agent interactive process state. The minimum state needed is:
- whether delete mode is active
- current page in delete mode
- selected session IDs
- whether the user is currently on the confirmation card
Session IDs, not row numbers, must be the source of truth after selection, so cross-page selection remains stable even if list ordering changes between renders.
## Card Actions
- `act:/delete-mode open`
- `act:/delete-mode toggle <session-id>`
- `act:/delete-mode page <n>`
- `act:/delete-mode confirm`
- `act:/delete-mode back`
- `act:/delete-mode submit`
- `act:/delete-mode cancel`
All delete-mode actions should update the card in place on Feishu. They should not dispatch as plain user commands.
## Error Handling
- Empty selection cannot proceed to confirmation; the card should stay in delete mode with a hint.
- Deleting the active session must be reported as blocked, not silently ignored.
- If a selected session no longer exists, report it in the result card rather than failing the whole batch.
- Cancel must always clear delete-mode state.
## Testing
- Rendering delete-mode cards with selected/unselected rows
- In-place toggle behavior and page persistence
- Confirmation card content
- Submit path deleting only selected session IDs
- Active-session protection in card-driven batch delete
- Explicit `/delete 1,3-5,8` continuing to work outside card mode
+175
View File
@@ -0,0 +1,175 @@
# Feishu Delete Card Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add a Feishu card-based multi-select delete flow triggered by `/delete` without changing existing explicit batch delete command semantics.
**Architecture:** Introduce a per-session delete-mode state keyed by `sessionKey`, render a dedicated delete-selection card and confirmation card, and route Feishu card `act:` callbacks through new delete-mode actions. Keep `/list` unchanged and keep explicit `/delete <args>` command execution on the existing code path.
**Tech Stack:** Go 1.24, `core.Engine`, shared card builder utilities, Feishu interactive cards, Go `testing`.
---
### Task 1: Add failing tests for delete-mode card flow
**Files:**
- Modify: `core/engine_test.go`
**Step 1: Write the failing test**
Add tests for:
- `/delete` with no args on a card-capable platform renders delete-mode card instead of usage text
- delete-mode list toggles selected session IDs through card actions
- confirmation card lists selected sessions
- submit deletes only selected sessions and clears delete-mode state
- cancel returns to normal list/current view and clears state
**Step 2: Run test to verify it fails**
Run: `go test ./core -run 'TestCmdDelete|TestDeleteMode'`
Expected: FAIL because delete-mode state and card actions do not exist.
**Step 3: Write minimal implementation**
Add the smallest delete-mode state and rendering hooks required for the first test to pass before expanding behavior.
**Step 4: Run test to verify it passes**
Run: `go test ./core -run 'TestCmdDelete|TestDeleteMode'`
Expected: PASS for the implemented slice.
**Step 5: Commit**
```bash
git add core/engine_test.go core/engine.go core/i18n.go
git commit -m "feat: add delete mode card flow scaffolding"
```
### Task 2: Implement delete-mode state and card rendering
**Files:**
- Modify: `core/engine.go`
- Modify: `core/i18n.go`
- Test: `core/engine_test.go`
**Step 1: Write the failing test**
Add tests covering:
- selected rows remain selected across pagination
- delete-mode footer buttons enable confirmation only when selection exists
- confirmation card back button preserves selection
**Step 2: Run test to verify it fails**
Run: `go test ./core -run 'TestDeleteMode'`
Expected: FAIL on missing state transitions or incorrect card rendering.
**Step 3: Write minimal implementation**
Implement:
- delete-mode state structure
- render function for delete-mode list
- render function for confirmation/result cards
- helper to resolve display names from selected session IDs
**Step 4: Run test to verify it passes**
Run: `go test ./core -run 'TestDeleteMode'`
Expected: PASS
**Step 5: Commit**
```bash
git add core/engine.go core/engine_test.go core/i18n.go
git commit -m "feat: render delete selection cards"
```
### Task 3: Wire Feishu card actions to delete-mode behavior
**Files:**
- Modify: `core/engine.go`
- Test: `core/engine_test.go`
- Inspect: `platform/feishu/feishu.go`
**Step 1: Write the failing test**
Add tests for:
- `act:/delete-mode toggle <id>`
- `act:/delete-mode confirm`
- `act:/delete-mode back`
- `act:/delete-mode submit`
- `act:/delete-mode cancel`
**Step 2: Run test to verify it fails**
Run: `go test ./core -run 'TestDeleteMode'`
Expected: FAIL because action dispatch does not recognize delete-mode actions.
**Step 3: Write minimal implementation**
Update `handleCardNav` and `executeCardAction` to:
- mutate delete-mode state in place
- re-render the correct card after each action
- clear state on submit/cancel
**Step 4: Run test to verify it passes**
Run: `go test ./core -run 'TestDeleteMode'`
Expected: PASS
**Step 5: Commit**
```bash
git add core/engine.go core/engine_test.go
git commit -m "feat: wire delete mode card actions"
```
### Task 4: Reuse deletion logic and protect edge cases
**Files:**
- Modify: `core/engine.go`
- Test: `core/engine_test.go`
**Step 1: Write the failing test**
Add tests for:
- active session in selected set is blocked and reported
- missing session ID in selected set is reported without aborting the whole batch
- explicit `/delete 1,2,3` and `/delete 1,3-5,8` still use command parsing path
**Step 2: Run test to verify it fails**
Run: `go test ./core -run 'TestCmdDelete|TestDeleteMode'`
Expected: FAIL on batch execution/reporting edge cases.
**Step 3: Write minimal implementation**
Refactor deletion helpers so card-mode submit can delete by selected session ID while sharing reply/result formatting with the command path.
**Step 4: Run test to verify it passes**
Run: `go test ./core -run 'TestCmdDelete|TestDeleteMode'`
Expected: PASS
**Step 5: Commit**
```bash
git add core/engine.go core/engine_test.go
git commit -m "feat: execute delete mode batch removal"
```
### Task 5: Run broader verification
**Files:**
- Test: `core/engine_test.go`
- Test: `platform/feishu/platform_test.go`
**Step 1: Run core verification**
Run: `go test ./core/...`
Expected: PASS
**Step 2: Run repository verification**
Run: `go test ./...`
Expected: PASS
@@ -0,0 +1,105 @@
# Multi-Workspace Feature Design
## Overview
Enable a single cc-connect bot (one Slack token) to serve multiple workspaces, with the channel determining which Claude Code working directory and session to use.
## Config
```toml
[[projects]]
name = "claude"
mode = "multi-workspace"
base_dir = "~/workspace"
[projects.agent]
type = "claudecode"
permission_mode = "yolo"
[[projects.platforms]]
type = "slack"
bot_token = "xoxb-..."
app_token = "xapp-..."
```
- `mode = "multi-workspace"` enables the feature. Omitting or `"single"` preserves current behavior.
- `base_dir` is the parent directory where workspaces live. Replaces `work_dir` on the agent.
- Agent config has no `work_dir` — resolved per-channel at runtime.
## Workspace Resolution Flow
When a message arrives in a channel:
1. **Check bindings** — look up `workspace_bindings.json` for an existing channel-to-workspace mapping.
2. **Convention match** — if no binding, check if `<base_dir>/<channel-name>/` exists. If yes, auto-bind and confirm:
> "Found `~/workspace/model-profiler` matching this channel. Binding workspace and starting session... Ready."
3. **Ask for repo** — if no match, reply:
> "No workspace found for this channel. What repo should I clone?"
User provides URL, bot confirms:
> "I'll clone `org/repo` to `~/workspace/repo-name` and bind to this channel. OK?"
4. **Clone and bind** — on confirmation, clone the repo, save the binding, spawn agent subprocess. Explicit feedback throughout:
> "Cloning `github.com/org/repo` to `~/workspace/repo-name`..."
> "Clone complete. Binding workspace to this channel... Ready."
### Binding Storage
Persisted in `~/.cc-connect/workspace_bindings.json`:
```json
{
"project:claude": {
"C0AKYKUF75K": {
"channel_name": "model-profiler",
"workspace": "/home/leigh/workspace/model-profiler",
"bound_at": "2026-03-12T10:00:00Z"
}
}
}
```
## Agent Subprocess Management
Engine maintains `workspaceAgents map[string]*workspaceState` keyed by workspace path. Each `workspaceState` holds the agent subprocess, its SessionManager, and a `lastActivity` timestamp.
### Lifecycle
1. **Spawn on first message** — start a Claude Code subprocess with `work_dir` set to the resolved workspace.
2. **Resume on subsequent messages** — reuse the running subprocess with saved session ID.
3. **Idle reap** — background goroutine checks `lastActivity` every minute. Subprocesses idle >15 minutes are stopped. Session ID is preserved so the next message transparently restarts.
4. **Graceful shutdown** — on bot shutdown, stop all subprocesses cleanly.
### Session Management
Each workspace gets its own SessionManager instance with a separate JSON file (same naming scheme as today: `project_hash.json`). Named sessions within a workspace work exactly as they do now.
## Message Routing Changes
In `Engine.handleMessage`, the multi-workspace path inserts before the existing flow:
1. **Extract channel ID** from the message's session key (`slack:channelID:userID`).
2. **Resolve workspace** — look up binding, convention match, or trigger init flow.
3. **If no workspace resolved** (init flow in progress) — handle the init conversation directly, don't forward to any agent.
4. **If workspace resolved** — get or spawn the agent subprocess for that workspace, then continue with existing message processing.
`interactiveStates` gets keyed by workspace+sessionKey (rather than just sessionKey) so the same user in different channels hits different agent processes.
### New Commands
- `/workspace` — show current channel's bound workspace
- `/workspace init <url>` — clone and bind
- `/workspace unbind` — remove binding
- `/workspace list` — show all bindings
Existing commands (`/sessions`, `/model`, etc.) work per-workspace.
## Error Handling & Edge Cases
- **Unbound channel, bot mentioned** — bot asks for repo URL. No agent forwarding until binding is established.
- **Clone fails** (bad URL, auth, disk) — bot reports error and asks user to try again. No partial binding saved.
- **Workspace directory deleted externally** — on next message, bot detects missing directory, removes the binding, re-enters init flow: "Workspace `~/workspace/foo` no longer exists. What repo should I clone?"
- **Agent subprocess crashes** — restart on next message using saved session ID (same as current behavior).
- **Bot in unwanted channel** — without binding or matching directory, it just asks for a repo. User can ignore or remove the bot.
## Architecture: Approach 1 (Engine-level multiplexing)
The Engine itself handles multi-workspace routing. No new meta-engine or wrapper layers. The multi-workspace logic is gated behind the `mode` config field, so single-workspace projects are completely unaffected.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
{
"planPath": "docs/plans/2026-03-12-multi-workspace-plan.md",
"tasks": [
{"id": 1, "subject": "Task 1: Add config fields", "status": "pending"},
{"id": 2, "subject": "Task 2: Workspace binding persistence", "status": "pending", "blockedBy": [1]},
{"id": 3, "subject": "Task 3: Workspace state and idle reaper", "status": "pending", "blockedBy": [1]},
{"id": 4, "subject": "Task 4: Channel name resolution in Slack platform", "status": "pending"},
{"id": 5, "subject": "Task 5: Engine multi-workspace fields and constructor", "status": "pending", "blockedBy": [2, 3]},
{"id": 6, "subject": "Task 6: Workspace resolution logic", "status": "pending", "blockedBy": [4, 5]},
{"id": 7, "subject": "Task 7: Init flow conversation handler", "status": "pending", "blockedBy": [6]},
{"id": 8, "subject": "Task 8: Wire multi-workspace routing into handleMessage", "status": "pending", "blockedBy": [6, 7]},
{"id": 9, "subject": "Task 9: Workspace commands", "status": "pending", "blockedBy": [6]},
{"id": 10, "subject": "Task 10: Wire multi-workspace in main.go", "status": "pending", "blockedBy": [5]},
{"id": 11, "subject": "Task 11: Integration testing", "status": "pending", "blockedBy": [8, 9, 10]},
{"id": 12, "subject": "Task 12: Update config.example.toml and verify build", "status": "pending", "blockedBy": [11]}
],
"lastUpdated": "2026-03-12T01:30:00Z"
}
+139
View File
@@ -0,0 +1,139 @@
# Usage Command Design
**Date:** 2026-03-12
**Goal:** Add a built-in `/usage` command that reports model/account quota usage, starting with Codex running under ChatGPT OAuth, while keeping the retrieval path generic so other agents can plug in later.
## Scope
- Add a new built-in slash command: `/usage`.
- The command is independent from `/status` and `/doctor`.
- Usage retrieval is exposed as an optional agent capability, not hardcoded into the engine for a single vendor.
- First implementation targets the Codex agent when local ChatGPT OAuth credentials are available in `~/.codex/auth.json`.
- Unsupported agents should return a clear “not supported” style message rather than failing the whole command system.
## Architecture
### Command Layer
`core/engine.go` will register and dispatch `/usage` as a normal built-in command. The engine should not know how ChatGPT, Gemini, or any future provider exposes quota data. It only detects whether the current agent implements a usage-reporting interface and formats the response.
### Agent Capability Layer
Add a new optional interface in `core/interfaces.go`, for example:
- `UsageReporter`
- a method such as `GetUsage(context.Context) (*UsageReport, error)`
The report type should be generic enough to cover multiple providers:
- provider/agent name
- subject or account label
- plan type / tier if available
- one or more rate-limit buckets/windows
- optional credits/balance fields
- raw provider-specific metadata only if needed for debugging
This keeps future integrations local to each agent package.
### Codex Implementation
`agent/codex` will implement the new interface by:
1. Reading `~/.codex/auth.json`
2. Extracting:
- `tokens.access_token`
- `tokens.account_id`
3. Calling:
- `GET https://chatgpt.com/backend-api/wham/usage`
4. Passing headers:
- `Authorization: Bearer <access_token>`
- `ChatGPT-Account-Id: <account_id>`
- `User-Agent: codex-cli`
5. Mapping the JSON response into the generic usage report
If `auth.json` is missing, fields are absent, or the HTTP call fails, the agent should return a normal error so `/usage` can present a concise failure message.
## Data Model
The generic report should support at least:
- identity:
- provider
- account_id
- user/email if available
- plan:
- plan_type
- standard rate limits:
- allowed
- limit_reached
- primary window
- secondary window
- review/code-review limits:
- same window shape when available
- credits:
- has_credits
- unlimited
- balance
Each window should preserve:
- used percent
- total window seconds
- reset-after seconds
- reset timestamp if supplied
This is enough for the current ChatGPT OAuth response and still general enough for other providers with multiple quota windows.
## Output Format
The first output version should be plain text and compact. Suggested shape:
- title line with agent/provider
- plan line
- standard usage section
- code review usage section if present
- credits section if present
For each window:
- whether requests are currently allowed
- whether the limit is reached
- used percent
- reset timing
Prefer rendering both relative time and absolute time if easy to do consistently. If absolute rendering is added, it should use local time formatting already used by the project, or a simple RFC3339 fallback.
## Error Handling
- Agent does not implement usage reporting:
- reply with a user-facing “current agent does not support `/usage`
- Codex auth file missing:
- explain that ChatGPT OAuth login data was not found
- Token/account id missing:
- explain credentials are incomplete
- HTTP non-200:
- include status code in logs; show concise failure text to user
- JSON decode failure:
- report provider response parse failure
Do not expose bearer tokens or raw auth file contents in user-visible output or logs.
## Testing
Minimum tests should cover:
- command dispatch recognizes `/usage`
- engine returns unsupported message when agent lacks the interface
- engine formats a successful generic usage report
- Codex usage fetch maps a representative `wham/usage` payload correctly
- Codex usage fetch errors on missing auth file / missing token fields
Network-dependent tests should use injected transport or `httptest`, not live requests.
## Non-Goals
- No merging into `/status` or `/doctor`
- No card UI in the first version
- No polling or background caching in the first version
- No support for non-Codex agents in this change beyond the shared interface
+155
View File
@@ -0,0 +1,155 @@
# Usage Command Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add a built-in `/usage` command with a generic agent usage-reporting interface, and implement the first provider-backed version for Codex using ChatGPT OAuth quota data.
**Architecture:** The engine gains a new built-in command that depends only on an optional `UsageReporter` interface. Agents that implement the interface return a generic `UsageReport`; the engine formats that into text. The Codex agent reads ChatGPT OAuth credentials from `~/.codex/auth.json`, calls the `wham/usage` endpoint, and maps the response into the generic structure.
**Tech Stack:** Go, built-in command dispatch in `core/engine.go`, optional agent interfaces in `core/interfaces.go`, HTTP/JSON via Go standard library, table-driven tests.
---
### Task 1: Add generic usage-reporting types
**Files:**
- Modify: `core/interfaces.go`
**Step 1: Add the failing interface usage surface**
Define:
- `UsageReporter`
- `UsageReport`
- `UsageBucket`
- `UsageWindow`
- `UsageCredits`
The structure should cover provider identity, plan/tier, generic allowed/limit-reached flags, one or more windows, and credits metadata.
**Step 2: Run targeted compile check**
Run: `go test ./core/...`
Expected: compile failures in engine/tests until the command layer is wired.
**Step 3: Keep the types minimal**
Do not add provider-specific fields unless they are broadly useful.
**Step 4: Re-run compile check**
Run: `go test ./core/...`
Expected: still failing only because `/usage` command is not yet fully integrated.
### Task 2: Add `/usage` built-in command and output formatter
**Files:**
- Modify: `core/engine.go`
- Modify: `core/i18n.go`
- Test: `core/engine_test.go`
**Step 1: Write/extend failing tests**
Add tests for:
- `/usage` dispatch to a supporting agent
- unsupported-agent message
- successful output formatting with representative report data
**Step 2: Run the targeted tests**
Run: `go test ./core/... -run 'Test.*Usage|TestHandleCommand'`
Expected: FAIL because the command is not registered yet.
**Step 3: Implement command plumbing**
Update:
- built-in command registration
- command dispatch switch
- bot command listing
- help/i18n text
Add a `cmdUsage` implementation that:
- type-asserts `UsageReporter`
- fetches usage with timeout
- formats a concise text response
**Step 4: Run the targeted tests again**
Run: `go test ./core/... -run 'Test.*Usage|TestHandleCommand'`
Expected: PASS
### Task 3: Implement Codex usage retrieval against ChatGPT OAuth
**Files:**
- Modify: `agent/codex/codex.go`
- Add or Modify: `agent/codex/usage.go`
- Test: `agent/codex/usage_test.go`
**Step 1: Write failing tests for the mapper/fetcher**
Cover:
- successful parsing of a representative `wham/usage` payload
- missing `auth.json`
- missing token/account fields
- HTTP error response
**Step 2: Run the targeted tests**
Run: `go test ./agent/codex -run 'Test.*Usage'`
Expected: FAIL because the implementation does not exist yet.
**Step 3: Implement minimal production code**
Add:
- auth file path resolver
- auth JSON loader
- HTTP request builder
- response DTOs
- mapping into `core.UsageReport`
Prefer dependency injection for:
- auth file path / reader
- HTTP client / transport
So tests avoid live network and real user files.
**Step 4: Run the targeted tests again**
Run: `go test ./agent/codex -run 'Test.*Usage'`
Expected: PASS
### Task 4: End-to-end verification and cleanup
**Files:**
- Modify: `README.md`
- Modify: `README.zh-CN.md`
- Modify: `CHANGELOG.md`
**Step 1: Document the new command**
Add `/usage` to command lists and a short explanation that support depends on the current agent.
**Step 2: Run focused test suites**
Run: `go test ./core/... ./agent/codex`
Expected: PASS
**Step 3: Run broader verification**
Run: `go test ./...`
Expected: PASS, or identify unrelated pre-existing failures clearly.
**Step 4: Commit**
Run:
```bash
git add core/interfaces.go core/engine.go core/i18n.go core/engine_test.go agent/codex/codex.go agent/codex/usage.go agent/codex/usage_test.go README.md README.zh-CN.md CHANGELOG.md docs/plans/2026-03-12-usage-design.md docs/plans/2026-03-12-usage.md
git commit -m "feat: add usage command for codex oauth"
```
@@ -0,0 +1,118 @@
# Session Resilience Design
**Date:** 2026-03-13
**Status:** Approved
**Branch:** feat/multi-workspace
## Problem
Multi-workspace mode introduces long-lived, concurrent Claude Code sessions that are reaped on idle and resumed on demand. Several failure modes cause silent context loss ("context rot"):
1. **CWD mismatch** — workspace paths that differ by trailing slash, symlink, or relative segment map to different Claude Code session directories, causing resume to silently start a fresh session
2. **Resume failure** — when a session's context is too large, `--resume` fails with "Prompt is too long" and the session becomes permanently broken until manual `!new`
3. **Invisible context degradation** — users have no signal that context is filling up until Claude starts forgetting things
4. **Silent failures** — session lifecycle events (spawn, resume, reap, failure) lack diagnostic logging
## Design
### 1. Path Normalization
**Helper:** `normalizeWorkspacePath(path string) string` in `workspace_state.go`
```
filepath.Clean(path) → filepath.EvalSymlinks(cleanedPath)
```
If `EvalSymlinks` fails (path doesn't exist yet), fall back to `filepath.Clean` only.
**Applied at two sites:**
- `workspacePool.GetOrCreate(workspace)` — normalize the key before map lookup/insert
- Workspace binding resolution — normalize before the workspace string enters the system
**Logging:** `slog.Debug("workspace path normalized", "original", path, "normalized", result)` when normalization changes the input.
### 2. Resume Failure → Fresh Session Fallback
**Location:** `getOrCreateInteractiveStateWith()` in engine.go
**Current behavior:** `StartSession` failure → state with nil `agentSession` → broken until `!new`.
**New behavior:**
1. If `StartSession` fails AND `session.AgentSessionID != ""` (resume attempt):
- Log failure with diagnostics: session ID, error message, cwd
- Clear `session.AgentSessionID` and save
- Retry `agent.StartSession(ctx, "")` for a fresh session
- Post platform notification: *"Session context was too large to resume — starting fresh. Project context is preserved in CLAUDE.md."*
2. If fresh retry also fails → fall through to existing nil-state behavior
3. If original call was already fresh (`AgentSessionID == ""`) → no retry, fall through as today
**Notification:** Send via `p.Send(ctx, replyCtx, msg)` — both are available on the `interactiveState` being constructed.
### 3. Context Consumption Indicator
**Dual-track approach with logging to compare accuracy over time.**
#### Track A: SDK token counts (accurate, cc-connect-owned)
- In `processInteractiveEvents`, parse `result` events for `input_tokens` usage
- Store cumulative `input_tokens` on the `interactiveState` (updated each turn)
- Compute percentage: `input_tokens / 200_000 * 100` (model context window)
- Append `[ctx: XX%]` to every message relayed to the platform
#### Track B: Claude self-report (approximate, for comparison)
- Add to system prompt via `--append-system-prompt`: instruction to append `[ctx: ~XX%]` to every response
- Parse the self-reported value from Claude's output before relaying
#### Logging
On every turn that has both values:
```
slog.Info("context_usage",
"sdk_pct", sdkPct,
"self_reported_pct", selfReportedPct,
"session_key", sessionKey,
"input_tokens", inputTokens)
```
Over time, compare drift to decide whether the system prompt instruction adds value.
#### Display
Appended to every visible message relayed to the platform:
```
Here's the refactored auth module...
[ctx: 62%]
```
If no token data available yet (first message), skip the indicator.
### 4. Diagnostic Logging
Structured `slog` logging at key lifecycle points:
| Event | Level | Fields |
|-------|-------|--------|
| Session spawn | Info | normalized cwd, session ID (or "new"), model |
| Session resume | Info | session ID, JSONL file path, file size |
| Resume failure | Error | session ID, error, stderr, cwd, JSONL file size |
| Fresh fallback | Warn | original session ID, new session ID, cwd |
| Idle reap | Info | session key, workspace path, idle duration, token count at reap |
| Context per-turn | Info | session key, input_tokens, sdk_pct, self_reported_pct |
| Path normalization | Debug | original path, normalized path (only when changed) |
**JSONL file size:** Resolve via `findProjectDir` + stat at resume time. Log even if file not found (indicates cwd mismatch).
## Non-Goals
- **Proactive compaction** — likely to cause more trouble than it's worth; the context indicator gives users agency to compact manually
- **Session summary → new session pattern** — more robust but significantly more implementation work; revisit if resume-with-fallback proves insufficient
- **Disk/memory monitoring** — out of scope; can be added as operational tooling later
## Implementation Order
1. Path normalization (prerequisite for everything else being reliable)
2. Diagnostic logging (needed to verify the other changes work)
3. Resume failure fallback (highest-value fix)
4. Context consumption indicator (most complex, benefits from logging already being in place)
@@ -0,0 +1,793 @@
# Session Resilience Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.
**Goal:** Eliminate silent session context loss in multi-workspace mode by normalizing paths, handling resume failures gracefully, surfacing context consumption to users, and adding diagnostic logging.
**Architecture:** Four independent changes layered bottom-up: path normalization (prevents mismatches), diagnostic logging (makes failures visible), resume fallback (auto-recovers from broken resumes), context indicator (gives users agency over compaction).
**Tech Stack:** Go, Claude Code CLI (stream-json protocol), slog structured logging
**Design doc:** `docs/plans/2026-03-13-session-resilience-design.md`
---
### Task 1: Add `normalizeWorkspacePath` helper
**Files:**
- Modify: `core/workspace_state.go`
- Create: `core/workspace_state_test.go` (add test cases)
**Step 1: Write the failing test**
Add to `core/workspace_state_test.go`:
```go
func TestNormalizeWorkspacePath(t *testing.T) {
// Create a real temp directory for symlink tests
tmp := t.TempDir()
realDir := filepath.Join(tmp, "real-project")
if err := os.Mkdir(realDir, 0o755); err != nil {
t.Fatal(err)
}
symlink := filepath.Join(tmp, "link-project")
if err := os.Symlink(realDir, symlink); err != nil {
t.Skip("symlinks not supported")
}
tests := []struct {
name string
input string
want string
}{
{"trailing slash", realDir + "/", realDir},
{"double slash", filepath.Join(tmp, "real-project") + "//", realDir},
{"dot segment", filepath.Join(tmp, ".", "real-project"), realDir},
{"dotdot segment", filepath.Join(tmp, "real-project", "subdir", ".."), realDir},
{"symlink resolved", symlink, realDir},
{"nonexistent uses Clean only", "/nonexistent/path/./foo/../bar", "/nonexistent/path/bar"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := normalizeWorkspacePath(tt.input)
if got != tt.want {
t.Errorf("normalizeWorkspacePath(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
```
**Step 2: Run test to verify it fails**
Run: `go test ./core/ -run TestNormalizeWorkspacePath -v`
Expected: FAIL — `normalizeWorkspacePath` undefined
**Step 3: Write minimal implementation**
Add to `core/workspace_state.go`:
```go
import (
"log/slog"
"os"
"path/filepath"
)
// normalizeWorkspacePath cleans and resolves a workspace path to prevent
// mismatches caused by trailing slashes, symlinks, or relative segments.
// If the path cannot be resolved (e.g. doesn't exist yet), falls back to
// filepath.Clean only.
func normalizeWorkspacePath(path string) string {
cleaned := filepath.Clean(path)
resolved, err := filepath.EvalSymlinks(cleaned)
if err != nil {
// Path doesn't exist yet — best effort
return cleaned
}
if resolved != path {
slog.Debug("workspace path normalized", "original", path, "normalized", resolved)
}
return resolved
}
```
**Step 4: Run test to verify it passes**
Run: `go test ./core/ -run TestNormalizeWorkspacePath -v`
Expected: PASS
**Step 5: Commit**
```bash
git add core/workspace_state.go core/workspace_state_test.go
git commit -m "feat: add normalizeWorkspacePath helper for consistent pool keys"
```
---
### Task 2: Apply path normalization at entry points
**Files:**
- Modify: `core/workspace_state.go``GetOrCreate`
- Modify: `core/engine.go:5656,5681``resolveWorkspace` return values
- Modify: `core/engine.go:993``getOrCreateWorkspaceAgent`
**Step 1: Write failing tests**
Add to `core/workspace_state_test.go`:
```go
func TestWorkspacePoolNormalizesKeys(t *testing.T) {
tmp := t.TempDir()
realDir := filepath.Join(tmp, "project")
if err := os.Mkdir(realDir, 0o755); err != nil {
t.Fatal(err)
}
pool := newWorkspacePool(15 * time.Minute)
// Access with trailing slash
ws1 := pool.GetOrCreate(realDir + "/")
// Access without trailing slash
ws2 := pool.GetOrCreate(realDir)
if ws1 != ws2 {
t.Error("trailing slash created a different workspace state")
}
}
```
**Step 2: Run test to verify it fails**
Run: `go test ./core/ -run TestWorkspacePoolNormalizesKeys -v`
Expected: FAIL — two different states returned
**Step 3: Normalize in `GetOrCreate` and `Get`**
In `core/workspace_state.go`, modify `GetOrCreate`:
```go
func (p *workspacePool) GetOrCreate(workspace string) *workspaceState {
workspace = normalizeWorkspacePath(workspace)
p.mu.Lock()
defer p.mu.Unlock()
if s, ok := p.states[workspace]; ok {
return s
}
s := newWorkspaceState(workspace)
p.states[workspace] = s
return s
}
```
Modify `Get` similarly:
```go
func (p *workspacePool) Get(workspace string) *workspaceState {
workspace = normalizeWorkspacePath(workspace)
p.mu.RLock()
defer p.mu.RUnlock()
return p.states[workspace]
}
```
**Step 4: Normalize in `resolveWorkspace` returns**
In `core/engine.go`, at lines 5656 and 5681 where workspace paths are returned, wrap with normalization:
At line 5656:
```go
return normalizeWorkspacePath(b.Workspace), b.ChannelName, nil
```
At line 5681:
```go
normalized := normalizeWorkspacePath(candidate)
e.workspaceBindings.Bind(projectKey, channelID, channelName, normalized)
slog.Info("workspace auto-bound by convention",
"channel", channelName, "workspace", normalized)
return normalized, channelName, nil
```
**Step 5: Run tests**
Run: `go test ./core/ -run TestWorkspacePool -v`
Expected: PASS
**Step 6: Commit**
```bash
git add core/workspace_state.go core/workspace_state_test.go core/engine.go
git commit -m "feat: normalize workspace paths at pool and resolution entry points"
```
---
### Task 3: Add token usage fields to Event and parse in session
**Files:**
- Modify: `core/message.go:87-98` — add `InputTokens`, `OutputTokens` to `Event`
- Modify: `agent/claudecode/session.go:268-282` — parse usage from result JSON
**Step 1: Write failing test**
Add to a new file or existing test for claudecode session parsing. Since `handleResult` is on the unexported `claudeSession`, test via the event channel:
Create `agent/claudecode/session_test.go` test (or add to existing):
```go
func TestHandleResultParsesUsage(t *testing.T) {
// Simulate a result event JSON with usage data
raw := map[string]any{
"type": "result",
"result": "test response",
"session_id": "sess-123",
"usage": map[string]any{
"input_tokens": float64(50000),
"output_tokens": float64(1500),
},
}
cs := &claudeSession{
events: make(chan core.Event, 1),
ctx: context.Background(),
done: make(chan struct{}),
}
cs.alive.Store(true)
cs.handleResult(raw)
evt := <-cs.events
if evt.InputTokens != 50000 {
t.Errorf("InputTokens = %d, want 50000", evt.InputTokens)
}
if evt.OutputTokens != 1500 {
t.Errorf("OutputTokens = %d, want 1500", evt.OutputTokens)
}
}
```
**Step 2: Run test to verify it fails**
Run: `go test ./agent/claudecode/ -run TestHandleResultParsesUsage -v`
Expected: FAIL — `InputTokens` field doesn't exist on Event
**Step 3: Add fields to Event**
In `core/message.go`, add to the `Event` struct:
```go
InputTokens int // populated for EventResult — total input tokens this turn
OutputTokens int // populated for EventResult — output tokens this turn
```
**Step 4: Parse usage in handleResult**
In `agent/claudecode/session.go`, modify `handleResult`:
```go
func (cs *claudeSession) handleResult(raw map[string]any) {
var content string
if result, ok := raw["result"].(string); ok {
content = result
}
if sid, ok := raw["session_id"].(string); ok && sid != "" {
cs.sessionID.Store(sid)
}
var inputTokens, outputTokens int
if usage, ok := raw["usage"].(map[string]any); ok {
if v, ok := usage["input_tokens"].(float64); ok {
inputTokens = int(v)
}
if v, ok := usage["output_tokens"].(float64); ok {
outputTokens = int(v)
}
}
evt := core.Event{
Type: core.EventResult,
Content: content,
SessionID: cs.CurrentSessionID(),
Done: true,
InputTokens: inputTokens,
OutputTokens: outputTokens,
}
select {
case cs.events <- evt:
case <-cs.ctx.Done():
return
}
}
```
**Step 5: Run test to verify it passes**
Run: `go test ./agent/claudecode/ -run TestHandleResultParsesUsage -v`
Expected: PASS
**Step 6: Commit**
```bash
git add core/message.go agent/claudecode/session.go agent/claudecode/session_test.go
git commit -m "feat: parse token usage from Claude Code result events"
```
---
### Task 4: Track context percentage on interactiveState and append to messages
**Files:**
- Modify: `core/engine.go:192-202` — add `inputTokens` field to `interactiveState`
- Modify: `core/engine.go:1326-1352` — track tokens on EventResult
- Modify: `core/engine.go:1354+` — append `[ctx: XX%]` to relayed messages
**Step 1: Add field to interactiveState**
In `core/engine.go`, add to the `interactiveState` struct:
```go
type interactiveState struct {
// ... existing fields ...
inputTokens int // last known input_tokens from result event (context size proxy)
}
```
**Step 2: Update EventResult handler to track tokens and append indicator**
In `processInteractiveEvents`, in the `case EventResult:` block (around line 1326), after the existing session ID handling:
```go
case EventResult:
if event.SessionID != "" {
session.mu.Lock()
session.AgentSessionID = event.SessionID
session.mu.Unlock()
}
// Track context consumption
if event.InputTokens > 0 {
state.mu.Lock()
state.inputTokens = event.InputTokens
state.mu.Unlock()
}
fullResponse := event.Content
if fullResponse == "" && len(textParts) > 0 {
fullResponse = strings.Join(textParts, "")
}
if fullResponse == "" {
fullResponse = e.i18n.T(MsgEmptyResponse)
}
// Append context indicator
if event.InputTokens > 0 {
pct := event.InputTokens * 100 / 200_000
fullResponse += fmt.Sprintf("\n[ctx: %d%%]", pct)
}
// ... rest of existing EventResult handling
```
**Step 3: Also append indicator to intermediate messages (tool use, thinking)**
For every visible message sent during a turn, we need the indicator. The simplest approach: store the last known percentage on the state, and have a helper:
```go
func contextIndicator(inputTokens int) string {
if inputTokens <= 0 {
return ""
}
pct := inputTokens * 100 / 200_000
return fmt.Sprintf("\n[ctx: %d%%]", pct)
}
```
Append `contextIndicator(state.inputTokens)` to every `e.send()` call in the event loop for EventThinking, EventToolUse, and EventResult. Read `state.inputTokens` under the state mutex that's already being acquired.
**Step 4: Run existing tests**
Run: `go test ./core/ -v -count=1`
Expected: PASS (no test changes needed — this is additive to message content)
**Step 5: Commit**
```bash
git add core/engine.go
git commit -m "feat: append context consumption indicator [ctx: XX%] to relayed messages"
```
---
### Task 5: Add system prompt instruction for Claude self-reporting
**Files:**
- Modify: `core/interfaces.go:36-81` — append context self-report instruction to `AgentSystemPrompt()`
**Step 1: Add instruction to system prompt**
At the end of the `AgentSystemPrompt()` return string, before the closing backtick, add:
```go
## Context awareness
At the end of every message you send, append your estimate of your context window consumption as: [ctx: ~XX%]
This helps the user decide when to run /compact. Be honest if you're unsure, estimate conservatively.
```
**Step 2: Run existing tests**
Run: `go test ./core/ -v -count=1`
Expected: PASS
**Step 3: Commit**
```bash
git add core/interfaces.go
git commit -m "feat: instruct agent to self-report context usage for comparison logging"
```
---
### Task 6: Add dual-track context logging
**Files:**
- Modify: `core/engine.go` — EventResult handler, add structured log with both values
**Step 1: Parse self-reported percentage from response**
Add helper in `core/engine.go`:
```go
import "regexp"
var ctxSelfReportRe = regexp.MustCompile(`\[ctx:\s*~?(\d+)%\]`)
// parseSelfReportedCtx extracts the self-reported context percentage from a response.
// Returns -1 if not found.
func parseSelfReportedCtx(response string) int {
m := ctxSelfReportRe.FindStringSubmatch(response)
if m == nil {
return -1
}
v, _ := strconv.Atoi(m[1])
return v
}
```
**Step 2: Write test for parser**
```go
func TestParseSelfReportedCtx(t *testing.T) {
tests := []struct {
input string
want int
}{
{"some response\n[ctx: ~45%]", 45},
{"response [ctx: 80%]", 80},
{"no indicator", -1},
{"[ctx: ~100%] mid-text", 100},
}
for _, tt := range tests {
got := parseSelfReportedCtx(tt.input)
if got != tt.want {
t.Errorf("parseSelfReportedCtx(%q) = %d, want %d", tt.input, got, tt.want)
}
}
}
```
**Step 3: Run test to verify it fails, implement, verify pass**
Run: `go test ./core/ -run TestParseSelfReportedCtx -v`
**Step 4: Add structured logging in EventResult handler**
After computing the SDK percentage but before appending the indicator, add:
```go
if event.InputTokens > 0 {
sdkPct := event.InputTokens * 100 / 200_000
selfPct := parseSelfReportedCtx(fullResponse)
slog.Info("context_usage",
"session_key", sessionKey,
"sdk_pct", sdkPct,
"self_reported_pct", selfPct,
"input_tokens", event.InputTokens,
"output_tokens", event.OutputTokens,
)
}
```
**Step 5: Strip Claude's self-reported indicator before appending the real one**
So we don't show duplicate indicators, strip the self-reported one from the response before appending the SDK-based one:
```go
// Strip self-reported indicator (we replace it with the accurate SDK one)
fullResponse = ctxSelfReportRe.ReplaceAllString(fullResponse, "")
fullResponse = strings.TrimRight(fullResponse, "\n ")
fullResponse += fmt.Sprintf("\n[ctx: %d%%]", sdkPct)
```
**Step 6: Commit**
```bash
git add core/engine.go core/engine_test.go
git commit -m "feat: dual-track context usage logging (SDK vs self-reported)"
```
---
### Task 7: Add diagnostic logging to session lifecycle
**Files:**
- Modify: `core/engine.go:1097-1119` — spawn/resume logging
- Modify: `core/engine.go:259-283` — reap logging
- Modify: `agent/claudecode/session.go:42-117` — spawn logging with JSONL path/size
- Modify: `agent/claudecode/claudecode.go:195-224` — log cwd at StartSession
**Step 1: Enhanced spawn logging in `getOrCreateInteractiveStateWith`**
Replace the existing log at line 1118 with:
```go
slog.Info("session spawned",
"session_key", sessionKey,
"agent_session", session.AgentSessionID,
"is_resume", session.AgentSessionID != "",
"elapsed", startElapsed,
)
```
**Step 2: Add JSONL file size logging in `claudecode.StartSession`**
In `agent/claudecode/claudecode.go`, in `StartSession`, before calling `newClaudeSession`, add:
```go
if sessionID != "" {
// Log session file details for diagnostics
homeDir, _ := os.UserHomeDir()
absWorkDir, _ := filepath.Abs(a.workDir)
if homeDir != "" {
projectDir := findProjectDir(homeDir, absWorkDir)
sessionFile := filepath.Join(projectDir, sessionID+".jsonl")
if info, err := os.Stat(sessionFile); err == nil {
slog.Info("session resume attempt",
"session_id", sessionID,
"jsonl_path", sessionFile,
"jsonl_size_bytes", info.Size(),
"work_dir", absWorkDir,
)
} else {
slog.Warn("session file not found for resume",
"session_id", sessionID,
"expected_path", sessionFile,
"work_dir", absWorkDir,
"error", err,
)
}
}
}
```
**Step 3: Enhanced reap logging in `runIdleReaper`**
In `core/engine.go`, in the reap loop (around line 271), add idle duration:
```go
reaped := e.workspacePool.ReapIdle()
for _, ws := range reaped {
e.interactiveMu.Lock()
for key, state := range e.interactiveStates {
if state.workspaceDir == ws {
state.mu.Lock()
tokenCount := state.inputTokens
state.mu.Unlock()
slog.Info("session idle-reaped",
"session_key", key,
"workspace", ws,
"last_ctx_pct", tokenCount*100/200_000,
"input_tokens", tokenCount,
)
if state.agentSession != nil {
state.agentSession.Close()
}
delete(e.interactiveStates, key)
}
}
```
**Step 4: Log stderr on session process failure**
In `agent/claudecode/session.go`, the `readLoop` already logs stderr on failure (line 125). Enhance it:
```go
slog.Error("claudeSession: process failed",
"error", err,
"stderr", stderrMsg,
"work_dir", cs.workDir,
"session_id", cs.CurrentSessionID(),
)
```
**Step 5: Run all tests**
Run: `go test ./core/ ./agent/claudecode/ -v -count=1`
Expected: PASS
**Step 6: Commit**
```bash
git add core/engine.go agent/claudecode/session.go agent/claudecode/claudecode.go
git commit -m "feat: add diagnostic logging for session spawn, resume, reap, and failure"
```
---
### Task 8: Resume failure fallback with user notification
**Files:**
- Modify: `core/engine.go:1097-1105` — retry logic in `getOrCreateInteractiveStateWith`
**Step 1: Write failing test**
Add to `core/engine_test.go`:
```go
func TestResumeFailureFallsBackToFreshSession(t *testing.T) {
callCount := 0
agent := &stubAgent{
startSessionFunc: func(ctx context.Context, sessionID string) (core.AgentSession, error) {
callCount++
if sessionID != "" {
// Simulate resume failure
return nil, fmt.Errorf("Prompt is too long")
}
// Fresh session succeeds
return &stubAgentSession{alive: true}, nil
},
}
e := newTestEngine(t)
e.agent = agent
session := e.sessions.GetOrCreateActive("test:chan:user")
session.AgentSessionID = "old-session-id"
p := &stubPlatform{}
state := e.getOrCreateInteractiveState("test:chan:user", p, nil, session)
if state.agentSession == nil {
t.Fatal("expected agentSession to be non-nil after fallback")
}
if callCount != 2 {
t.Errorf("expected 2 StartSession calls (resume + fresh), got %d", callCount)
}
if session.AgentSessionID != "" {
t.Errorf("expected AgentSessionID cleared, got %q", session.AgentSessionID)
}
}
```
Note: This test may need adjustment to match the actual test helpers in the codebase. Check `core/engine_test.go` for the existing `stubAgent` and `newTestEngine` patterns and adapt accordingly.
**Step 2: Run test to verify it fails**
Run: `go test ./core/ -run TestResumeFailureFallsBackToFreshSession -v`
Expected: FAIL — current code doesn't retry
**Step 3: Implement retry logic**
Replace the error handling block in `getOrCreateInteractiveStateWith` (lines 1100-1104):
```go
startAt := time.Now()
agentSession, err := agent.StartSession(e.ctx, session.AgentSessionID)
startElapsed := time.Since(startAt)
if err != nil {
if session.AgentSessionID != "" {
// Resume failed — log diagnostics and retry with fresh session
slog.Error("session resume failed, falling back to fresh session",
"session_key", sessionKey,
"failed_session_id", session.AgentSessionID,
"error", err,
"elapsed", startElapsed,
)
// Clear the stale session ID
session.mu.Lock()
session.AgentSessionID = ""
session.mu.Unlock()
// Notify user
if p != nil {
go func() {
_ = p.Send(context.Background(), replyCtx,
"⚠️ Session context was too large to resume — starting fresh. Project context is preserved in CLAUDE.md.")
}()
}
// Retry with fresh session
freshStart := time.Now()
agentSession, err = agent.StartSession(e.ctx, "")
freshElapsed := time.Since(freshStart)
if err != nil {
slog.Error("fresh session also failed",
"session_key", sessionKey,
"error", err,
"elapsed", freshElapsed,
)
state = &interactiveState{platform: p, replyCtx: replyCtx, quiet: quietMode}
e.interactiveStates[sessionKey] = state
return state
}
slog.Info("fresh session started after resume failure",
"session_key", sessionKey,
"elapsed", freshElapsed,
)
} else {
slog.Error("failed to start interactive session",
"session_key", sessionKey,
"error", err,
"elapsed", startElapsed,
)
state = &interactiveState{platform: p, replyCtx: replyCtx, quiet: quietMode}
e.interactiveStates[sessionKey] = state
return state
}
}
```
**Step 4: Run test to verify it passes**
Run: `go test ./core/ -run TestResumeFailureFallsBackToFreshSession -v`
Expected: PASS
**Step 5: Run full test suite**
Run: `go test ./core/ ./agent/claudecode/ -v -count=1`
Expected: PASS
**Step 6: Commit**
```bash
git add core/engine.go core/engine_test.go
git commit -m "feat: auto-recover from resume failure with fresh session and user notification"
```
---
### Task 9: Final integration verification
**Files:** None — verification only
**Step 1: Run full test suite**
Run: `go test ./... -count=1`
Expected: PASS
**Step 2: Verify build**
Run: `go build ./...`
Expected: no errors
**Step 3: Review all changes**
Run: `git log --oneline main..HEAD`
Verify the commit sequence matches the plan:
1. `normalizeWorkspacePath` helper
2. Apply normalization at entry points
3. Token usage fields + parsing
4. Context indicator on messages
5. System prompt self-report instruction
6. Dual-track logging
7. Diagnostic lifecycle logging
8. Resume failure fallback
**Step 4: Final commit (if any fixups needed)**
```bash
git add -A && git commit -m "fix: address issues found during integration verification"
```
@@ -0,0 +1,15 @@
{
"planPath": "docs/plans/2026-03-13-session-resilience-plan.md",
"tasks": [
{"id": 7, "subject": "Task 1: Add normalizeWorkspacePath helper", "status": "pending"},
{"id": 8, "subject": "Task 2: Apply path normalization at entry points", "status": "pending", "blockedBy": [7]},
{"id": 9, "subject": "Task 3: Add token usage fields to Event and parse in session", "status": "pending"},
{"id": 10, "subject": "Task 4: Track context % on interactiveState and append to messages", "status": "pending", "blockedBy": [9]},
{"id": 11, "subject": "Task 5: Add system prompt instruction for self-reporting", "status": "pending"},
{"id": 12, "subject": "Task 6: Add dual-track context logging", "status": "pending", "blockedBy": [10, 11]},
{"id": 13, "subject": "Task 7: Add diagnostic logging to session lifecycle", "status": "pending"},
{"id": 14, "subject": "Task 8: Resume failure fallback with user notification", "status": "pending"},
{"id": 15, "subject": "Task 9: Final integration verification", "status": "pending", "blockedBy": [8, 12, 13, 14]}
],
"lastUpdated": "2026-03-13T00:00:00Z"
}
@@ -0,0 +1,83 @@
# ACP 适配层设计(草案)
本文描述在 cc-connect 中增加 **Agent Client ProtocolACP** 适配的可行方案,目标是让 **已实现 ACP Agent 端** 的上游进程(见 [官方 Agents 列表](https://agentclientprotocol.com/get-started/agents))能通过 **统一协议**接入现有 `core.Engine`,减少为每个 CLI 单独维护解析逻辑的成本。
## 1. 背景与术语
- **ACP**:基于 JSON-RPC 的标准,用于 **Client(如编辑器)↔ Agent(编码助手进程)** 通信;与 IM 无关。
- **对 cc-connect 的价值**:在 `agent/` 侧实现 **ACP Client**(连接子进程或 socket 上的 ACP Agent),将 ACP 消息映射为现有的 `core.Agent` / `core.AgentSession` / `core.Event`,从而使 **飞书 / Telegram 等平台** 与「任意兼容 ACP 的 Agent 后端」对接。
- **不在本文范围(可选二期)**:让 cc-connect **作为 ACP Agent 对外暴露**,供 Zed 等编辑器直连;需完整实现协议 Agent 侧,工作量更大。
## 2. 架构约束(与仓库规则一致)
- `core/` **不** import `agent/*`;新逻辑全部放在 `agent/acp/`(或 `agent/acpclient/`)。
- 通过 `core.RegisterAgent("acp", factory)``init()` 注册;`cmd/cc-connect/plugin_agent_acp.go` + `Makefile` + `config.example.toml` 与现有 agent 插件一致。
- 权限、会话、卡片等多为 Engine 已有能力;适配层专注 **协议 ↔ Event**
## 3. 目标与非目标
### 3.1 一期目标(MVP
- 配置驱动启动子进程:`command` + `args` + `work_dir` + `env`(与现有 agent options 风格一致)。
- Transport**stdio JSON-RPC**ACP 文档中最常见);后续再评估 HTTP/WebSocket。
- 映射能力(按优先级):
1. 会话生命周期:与 `StartSession` / `Close` / `CurrentSessionID` 对齐。
2. **Prompt turn**:用户文本(及后续可选图片/文件)→ ACP 对应方法;响应流 → `Event``EventResult``EventThinking`、增量文本等,与现有 Engine 消费方式一致)。
3. **工具调用与用户批准**:映射到 `EventPermission` + `RespondPermission`(若 ACP 方法名与字段与 core 不完全一致,在适配层做字段转换)。
- 单项目、单用户会话语义与现有一致:`session_key` 仍由 Platform 提供,ACP 侧使用独立 `sessionID` 字符串与 cc-connect 会话绑定策略需在实现阶段定稿(建议:cc-connect `sessionID` 传入 adapterACP session id 由子进程返回或持久化路径配置)。
### 3.2 明确延后(二期+
- ACP **File System / Terminal** 全量映射(若与 IM 展示模型差距大,可先降级为文本摘要或仅日志)。
- **Slash commands / Agent plan** 与 IM 命令体系的统一(可先忽略或透传为纯文本)。
- cc-connect **作为 ACP Server** 供编辑器连接。
## 4. 组件划分
| 组件 | 职责 |
|------|------|
| `agent/acp/agent.go` | 实现 `core.Agent``Name``StartSession``ListSessions``Stop` |
| `agent/acp/session.go` | 实现 `core.AgentSession``Send``Events``RespondPermission``Close` 等 |
| `agent/acp/rpc.go`(或 `transport_stdio.go` | stdio 上的 JSON-RPC 读写、request id、并发与取消 |
| `agent/acp/mapping.go` | ACP 通知/结果 → `core.Event``PermissionResult` ↔ ACP 工具批准结构 |
| 测试 | 子进程 mock:固定 JSON-RPC 回放 fixture,避免 CI 依赖真实 Cursor/Codex 二进制 |
## 5. 配置草案(`config.example.toml`
```toml
# [[projects]]
# [projects.agent]
# type = "acp"
# [projects.agent.options]
# command = "path/to/agent" # 或 npx / uvx 等
# args = [] # 可选
# # cwd 默认 work_direnv 可扩展
# # acp_transport = "stdio" # 默认;预留 "http" 等
```
具体字段名以实现时与 `config` 解析为准,需 **向后兼容**:未安装插件时 `no_acp` build tag 行为与现有 agent 一致。
## 6. 风险与依赖
- **协议版本**:需锁定所实现的 ACP schema 版本;上游变更时通过集成测试与 changelog 跟进。
- **Agent 差异**:列表中各产品对 ACP 子集支持不同;MVP 文档中写明「已验证」矩阵(至少 1~2 个开源/可脚本化 Agent)。
- **router / 代理场景**:若子进程同时向 stdout 打非 JSON 日志,会破坏流式解析;与 claudecode `router_url` 下禁用 `--verbose` 同类问题需在 ACP 层统一约束(仅 JSON-RPC 行写入协议通道)。
## 7. 实施顺序建议
1. 阅读官方 **Protocol / Session / Prompt / Content / Tool** 章节与 **Schema**,列出与 `core.Event` 的字段对照表。
2. 实现 stdio transport + 最小会话握手(无 UI)。
3. 打通一轮 prompt → 文本结果 → `EventResult`
4. 接入权限与工具事件;补 `engine` 层无需改动的验证测试。
5. 文档:`docs/` 简短用户说明 + `config.example.toml` 示例。
6. (可选)在 CI 中使用 mock server 跑 `go test ./agent/acp/...`
## 8. 参考链接
- [ACP Introduction](https://agentclientprotocol.com/get-started/introduction)
- [ACP Agents 列表](https://agentclientprotocol.com/get-started/agents)
- [Protocol Overview](https://agentclientprotocol.com/protocol/overview)(以官网当前版本为准)
---
*Status: design draft — 实现跟踪可在本文件追加「Implementation log」小节或单独 tasks JSON。*
+111
View File
@@ -0,0 +1,111 @@
# Integration Test Plan
Integration tests verify real agent-platform interactions using actual agent binaries
with a mock platform. Tests are gated by `//go:build integration` and excluded from
normal CI. Run with:
```bash
go test -tags=integration ./tests/integration/...
```
## Philosophy
- **Real agents, mocked platform**: Agents run as real subprocesses; platform is mocked
to record and verify all messages without network dependencies.
- **Agent pooling**: Agent instances are reused across tests to avoid per-test startup
overhead (Claude Code cold start ~3-6s).
- **Resilient assertions**: Use case-insensitive substring matching, generous timeouts,
and skip agents that fail due to auth/infra issues (e.g., OpenCode needs GitLab token).
---
## Implemented Cases
### Session Management
- [x] `TestNewSession_ClaudeCode` — New session spawns, agent responds
- [x] `TestNewSession_Codex` — Same for Codex
- [x] `TestListSessions_ShowsActiveSessions``/list` shows active sessions
- [x] `TestSwitchSession``/switch` changes active session
- [x] `TestStopCommand``/stop` interrupts active session
- [x] `TestNewSessionClearsContext` — After `/new`, prior context is cleared
- [x] `TestHistoryCommand``/history` returns conversation history
- [x] `TestConcurrentSessionIsolation` — Two sessions don't cross-talk
### Agent Interaction
- [x] `TestEventParsing_ThinkToolUse` — Tool calls (echo) are parsed and produce output
- [x] `TestMarkdownLongTextChunking` — Long responses chunked correctly
- [x] `TestPermissionModeSwitch``/mode yolo` and `/mode default` work
- [x] `TestAgentCodex` — Codex agent responds
- [x] `TestAgentCursor` — Cursor agent responds (⚠️ may respond in locale)
- [x] `TestAgentGemini` — ⚠️ Fails due to quota exhaustion in CI env
- [x] `TestAgentOpencode` — ⚠️ Requires GitLab auth, skipped
- [x] `TestSharedCasesAcrossAgents` — Same prompts validated across agents
- [x] `TestLongTextChunking` — Very long user input (5k+ chars) processed
### Commands & Built-ins
- [x] `TestShellCommand``/shell` executes (skips if `admin_from` not set)
- [x] `TestProviderSwitch``/provider list` works
### i18n
- [x] `TestLanguageSwitch``/lang zh` changes language (⚠️ may skip due to locale)
- [x] `TestEmptyMessage` — Whitespace-only messages handled gracefully
### Message Handling
- [x] `TestImageAttachmentRouting` — Image-bearing messages routed (⚠️ needs real image)
---
## Planned Cases (not yet implemented)
### Multi-Agent / Provider
- [ ] `TestSessionResume` — Send `/new`, reconnect to same session key, verify context preserved
- [ ] `TestProviderSwitchActual``/provider switch <name>` actually changes provider mid-session
- [ ] `TestModelSwitch``/model <name>` changes model; responses reflect new model
- [ ] `TestConcurrentMultiAgent` — Two different agent types active simultaneously
### Commands & Built-ins
- [ ] `TestCustomCommand` — Register and invoke a custom command
- [ ] `TestAliasCommand` — Create/use alias; verify substitution
- [ ] `TestDirCommand``/dir` navigates workspace; agent respects new directory
- [ ] `TestSearchCommand``/search <query>` invokes search
### Permission & Safety
- [ ] `TestPermissionPromptBypass``yolo` mode bypasses permission prompts; `default` shows them
- [ ] `TestSensitiveMessageRedaction` — Tokens/secrets in user input are redacted
- [ ] `TestBannedWordBlocking` — Banned-word messages rejected with feedback
### Message Handling
- [ ] `TestFileAttachmentRouting` — File attachments reach agent
- [ ] `TestVoiceMessageHandling` — Voice messages transcribed/processed or gracefully rejected
- [ ] `TestMarkdownParsing` — Markdown in agent responses rendered correctly
### Rate Limiting & Performance
- [ ] `TestIncomingRateLimit` — Rapid messages rate-limited; excess queued/rejected
- [ ] `TestOutgoingRateLimit` — Rapid agent output respects platform limits
- [ ] `TestSlowAgentTimeout` — Slow agent (>idle timeout) flagged or session reaped
### i18n
- [ ] `TestMultiLanguageResponses` — Same prompt in different language configs produces localized responses
### Error & Edge Cases
- [ ] `TestBadAgentOutput` — Malformed agent output handled gracefully (no panic)
- [ ] `TestAgentCrashRecovery` — Agent dies mid-session; engine detects, notifies, allows respawn
- [ ] `TestVeryLongAgentResponse` — Extremely long response (>50k chars); chunking works, no OOM
- [ ] `TestConcurrentSessionCreation` — Rapid session creation; keys unique, no state leakage
### ACP / Relay
- [ ] `TestACPMessageRelay` — ACP message relayed to agent; response returns via correct channel
- [ ] `TestRelaySessionKeyPreservation``CC_SESSION_KEY` propagated through relay; session continuity maintained
---
## Notes
- **Timeout guidelines**: Simple prompts ("say hi") — 30s; tool use — 60s; slow agents
(gemini, opencode) — 90s; long output — 120s.
- **Skip vs Fail**: Auth/infra failures (`Skip`) are expected in some environments;
code bugs should `Fatal`. Use `t.Skipf("reason")` for expected env issues.
- **Agent pool reuse**: The pool key includes `workDir`, so tests using the same workDir
share the same agent instance. Use `t.TempDir()` for isolation.
- **Parallel tests**: Use `t.Parallel()` for independent tests. Avoid parallel subtests
that share session keys to prevent race conditions.
+129
View File
@@ -0,0 +1,129 @@
# QQ 平台接入指南 / QQ Platform Setup Guide
cc-connect 通过 [OneBot v11](https://github.com/botuniverse/onebot-11) 协议连接 QQ,需要搭配一个 OneBot 实现(如 NapCat)使用。
cc-connect connects to QQ via the [OneBot v11](https://github.com/botuniverse/onebot-11) protocol. You need a OneBot implementation (e.g., NapCat) running alongside.
## 架构 / Architecture
```
QQ Client ←→ NapCat (OneBot v11) ←WebSocket→ cc-connect ←→ Agent (Claude Code / etc.)
```
## 前置条件 / Prerequisites
- 一个 QQ 账号用作机器人 / A QQ account to act as the bot
- [NapCat](https://github.com/NapNeko/NapCatQQ) 或其他 OneBot v11 实现 / NapCat or another OneBot v11 implementation
## 步骤 / Steps
### 1. 部署 NapCat / Deploy NapCat
推荐使用 Docker(最简单)/ Docker is recommended (easiest):
```bash
docker run -d \
--name napcat \
-e ACCOUNT=<你的QQ号> \
-p 3001:3001 \
-p 6099:6099 \
mlikiowa/napcat-docker:latest
```
首次启动需要扫码登录 / First launch requires QR code login:
```bash
docker logs -f napcat
```
在日志中找到二维码,用手机 QQ 扫码登录。
Find the QR code in the logs and scan it with your QQ mobile app.
### 2. 配置 NapCat 正向 WebSocket / Configure Forward WebSocket
打开 NapCat WebUI / Open the NapCat WebUI:
```
http://localhost:6099
```
在网络配置中:/ In network settings:
- 启用 **正向 WebSocket** (Forward WebSocket) / Enable **Forward WebSocket**
- 端口设为 `3001`(默认)/ Port: `3001` (default)
- 如果需要鉴权,设置 Access Token / Set Access Token if needed
### 3. 配置 cc-connect / Configure cc-connect
`config.toml` 中添加 QQ 平台 / Add QQ platform to `config.toml`:
```toml
[[projects.platforms]]
type = "qq"
[projects.platforms.options]
ws_url = "ws://127.0.0.1:3001" # NapCat 正向 WebSocket 地址
token = "" # 可选:Access Token(需与 NapCat 一致)
allow_from = "*" # 允许交互的 QQ 号,"*" 表示所有人
```
**`allow_from` 配置说明 / `allow_from` options:**
- `"*"` — 允许所有人 / Allow everyone
- `"12345"` — 仅允许 QQ 号 12345 / Only allow QQ user 12345
- `"12345,67890"` — 允许多个 QQ 号 / Allow multiple QQ users
### 4. 启动 / Start
```bash
cc-connect
```
看到如下日志表示连接成功 / You should see:
```
qq: connected to OneBot url=ws://127.0.0.1:3001
qq: logged in qq=123456789 nickname=MyBot
```
现在可以在 QQ 上私聊或群聊机器人了!
Now you can chat with the bot via QQ private or group messages!
## 群聊使用 / Group Chat
支持群聊消息。在群中发送消息时,机器人会以独立的会话(按用户区分)处理每个人的请求。
Group chat is supported. Each user gets their own independent session, even in group chats.
## 支持的消息类型 / Supported Message Types
| 类型 / Type | 接收 / Receive | 发送 / Send |
|------------|----------------|-------------|
| 文字 / Text | ✅ | ✅ |
| 图片 / Image | ✅ | ❌ (文本描述) |
| 语音 / Voice | ✅ (需配置 STT) | ❌ |
| @提及 / @mention | ✅ (忽略) | — |
## 常见问题 / FAQ
**Q: 连接失败?/ Connection failed?**
- 确认 NapCat 正在运行且端口正确 / Check that NapCat is running and port is correct
- 确认 NapCat 已启用正向 WebSocket / Verify Forward WebSocket is enabled in NapCat
- 如果设置了 Token,确保两边一致 / If using Token, ensure it matches on both sides
**Q: 收不到消息?/ Not receiving messages?**
- 检查 `allow_from` 配置,确认你的 QQ 号在允许列表中 / Check `allow_from` includes your QQ ID
- 查看 NapCat 日志确认消息是否正确转发 / Check NapCat logs for message forwarding
**Q: NapCat 掉线?/ NapCat disconnected?**
- NapCat 使用 NTQQ 协议,长时间挂机可能需要重新登录 / NapCat may need re-login after long periods
- 建议使用 Docker restart policy: `--restart unless-stopped`
## 其他 OneBot 实现 / Other OneBot Implementations
除了 NapCat,以下 OneBot v11 实现也应该兼容 / Besides NapCat, these should also work:
- [LLOneBot](https://github.com/LLOneBot/LLOneBot) — NTQQ 插件 / NTQQ plugin
- [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core) — 跨平台 / Cross-platform
- [OpenShamrock](https://github.com/whitechi73/OpenShamrock) — Xposed 模块 / Xposed module (Android)
只要支持正向 WebSocket 的 OneBot v11 实现都可以使用。
Any OneBot v11 implementation with Forward WebSocket support should work.
+136
View File
@@ -0,0 +1,136 @@
# QQ Bot 官方平台接入指南 / QQ Bot Official Platform Setup Guide
cc-connect 通过 [QQ 官方机器人 API v2](https://bot.q.qq.com/wiki/) 连接 QQ,无需第三方适配器,无需公网 IP。
cc-connect connects to QQ via the [official QQ Bot Platform API v2](https://bot.q.qq.com/wiki/). No third-party adapter needed, no public IP required.
## 与 QQ (OneBot) 的区别 / Difference from QQ (OneBot)
| | QQ Bot 官方 (`qqbot`) | QQ OneBot (`qq`) |
|--|----------------------|------------------|
| 协议 / Protocol | QQ 官方 API v2 | OneBot v11 (第三方) |
| 适配器 / Adapter | 不需要 / Not needed | 需要 NapCat 等 / Requires NapCat etc. |
| 封号风险 / Ban risk | 无 / None (腾讯官方) | 有 / Possible |
| 公网 IP / Public IP | 不需要 (WebSocket) | 不需要 (WebSocket) |
| 注册 / Registration | 需要开发者认证 / Developer verification required | 仅需 QQ 账号 / QQ account only |
| 群消息 / Group messages | 仅 @机器人 时 / Only when @mentioned | 所有消息 / All messages |
## 架构 / Architecture
```
QQ Open Platform ←WebSocket→ cc-connect ←→ Agent (Claude Code / etc.)
```
## 前置条件 / Prerequisites
1. 访问 [QQ 开放平台](https://q.qq.com) 注册开发者账号
Visit [QQ Open Platform](https://q.qq.com) and register a developer account
2. 创建机器人应用,获取 **AppID****AppSecret**
Create a bot application and obtain **AppID** and **AppSecret**
3. 在机器人管理页面配置权限和上线
Configure permissions and publish in the bot management page
## 步骤 / Steps
### 1. 创建机器人 / Create Bot
1. 登录 [QQ 开放平台](https://q.qq.com)
Log in to [QQ Open Platform](https://q.qq.com)
2. 点击 **创建机器人** → 填写基本信息
Click **Create Bot** → fill in basic information
3.**开发 → 开发设置** 中获取 `AppID``AppSecret`
Get `AppID` and `AppSecret` from **Development → Development Settings**
### 2. 配置 cc-connect / Configure cc-connect
`config.toml` 中添加 QQ Bot 平台 / Add QQ Bot platform to `config.toml`:
```toml
[[projects.platforms]]
type = "qqbot"
[projects.platforms.options]
app_id = "your-app-id" # 机器人 AppID
app_secret = "your-app-secret" # 机器人 AppSecret
sandbox = false # 使用沙箱环境(测试用)/ Use sandbox (for testing)
allow_from = "*" # 允许的用户 openid"*" 表示所有 / Allowed user openids, "*" for all
```
**配置项说明 / Configuration options:**
| 参数 / Option | 必填 / Required | 说明 / Description |
|---|---|---|
| `app_id` | ✅ | 机器人 AppID / Bot AppID |
| `app_secret` | ✅ | 机器人 AppSecret / Bot AppSecret |
| `sandbox` | ❌ | 使用沙箱 API(默认 false/ Use sandbox API (default false) |
| `allow_from` | ❌ | 允许的用户 openid 列表或 `"*"`(默认允许所有)/ Allowed user openids or `"*"` |
| `intents` | ❌ | 自定义事件意图位掩码 / Custom intents bitmask (advanced) |
### 3. 启动 / Start
```bash
cc-connect
```
看到如下日志表示连接成功 / You should see:
```
qqbot: connected to QQ Bot gateway sandbox=false
qqbot: gateway READY session_id=...
```
现在可以在 QQ 群聊中 @机器人 或私聊机器人了!
Now you can @mention the bot in group chats or send private messages!
## 群聊使用 / Group Chat
在群聊中,机器人**仅在被 @提及 时**收到消息。这是 QQ 官方 API 的限制。
In group chats, the bot **only receives messages when @mentioned**. This is a limitation of the official QQ Bot API.
每个用户在每个群中拥有独立的会话。
Each user gets an independent session per group.
## 私聊 / Private Messages (C2C)
支持一对一私聊消息,无需 @提及
One-on-one private messages are supported without @mention.
## 支持的消息类型 / Supported Message Types
| 类型 / Type | 接收 / Receive | 发送 / Send |
|------------|----------------|-------------|
| 文字 / Text | ✅ | ✅ |
| 图片 / Image | ✅ | ❌ |
| 语音 / Voice | ❌ | ❌ |
| @提及 / @mention | ✅ (自动剥离) | — |
## 常见问题 / FAQ
**Q: 连接失败?/ Connection failed?**
- 确认 `app_id``app_secret` 是否正确 / Verify `app_id` and `app_secret` are correct
- 检查网络是否能访问 `api.sgroup.qq.com` / Check network access to `api.sgroup.qq.com`
- 如果使用沙箱环境,确认 `sandbox = true` / If using sandbox, set `sandbox = true`
**Q: 收不到群消息?/ Not receiving group messages?**
- 群消息仅在 @机器人 时触发 / Group messages require @mention
- 确认机器人已被添加到群中 / Verify the bot has been added to the group
- 检查 `allow_from` 配置 / Check `allow_from` configuration
**Q: 提示 token 获取失败?/ Token acquisition failed?**
- 确认 `app_secret` 正确 / Verify `app_secret` is correct
- 检查机器人是否已上线(未上线只能使用沙箱)/ Check if the bot is published (unpublished bots can only use sandbox)
**Q: 断线重连?/ Reconnection?**
- cc-connect 内置自动重连机制,断线后会自动尝试恢复(最多 30 次)
- cc-connect has built-in automatic reconnection with resume support (up to 30 attempts)
## 沙箱环境 / Sandbox
开发测试时可以使用沙箱环境,设置 `sandbox = true`。沙箱环境使用独立的 API 端点 (`sandbox.api.sgroup.qq.com`),不影响生产环境。
For development and testing, set `sandbox = true`. The sandbox uses a separate API endpoint (`sandbox.api.sgroup.qq.com`) and doesn't affect production.
+243
View File
@@ -0,0 +1,243 @@
{
"_metadata": {
"major_version": 2,
"minor_version": 1
},
"display_information": {
"name": "CC-Connect",
"description": "Bridge between AI coding agents and Slack",
"background_color": "#1a1a2e"
},
"features": {
"bot_user": {
"display_name": "CC-Connect",
"always_online": true
},
"slash_commands": [
{
"command": "/ps",
"description": "Send a P.S. to the running task",
"usage_hint": "[message]",
"should_escape": false
},
{
"command": "/new",
"description": "Start a new session",
"usage_hint": "[name]",
"should_escape": false
},
{
"command": "/list",
"description": "List agent sessions",
"should_escape": false
},
{
"command": "/switch",
"description": "Resume a session by its list number",
"usage_hint": "<number>",
"should_escape": false
},
{
"command": "/delete",
"description": "Delete sessions by list number(s)",
"usage_hint": "<number>|1,2,3|3-7|1,3-5,8",
"should_escape": false
},
{
"command": "/name",
"description": "Name a session for easy identification",
"usage_hint": "[number] <text>",
"should_escape": false
},
{
"command": "/current",
"description": "Show current active session",
"should_escape": false
},
{
"command": "/search",
"description": "Search sessions by name or ID",
"usage_hint": "<keyword>",
"should_escape": false
},
{
"command": "/history",
"description": "Show last n messages",
"usage_hint": "[n]",
"should_escape": false
},
{
"command": "/model",
"description": "View or switch model",
"usage_hint": "[name]",
"should_escape": false
},
{
"command": "/mode",
"description": "View or switch permission mode",
"usage_hint": "[default|edit|plan|yolo]",
"should_escape": false
},
{
"command": "/stop",
"description": "Stop current execution",
"should_escape": false
},
{
"command": "/compress",
"description": "Compress conversation context",
"should_escape": false
},
{
"command": "/quiet",
"description": "Toggle thinking and tool progress display",
"usage_hint": "[global]",
"should_escape": false
},
{
"command": "/status",
"description": "Show system status",
"should_escape": false
},
{
"command": "/usage",
"description": "Show account and model quota usage",
"should_escape": false
},
{
"command": "/help",
"description": "Show available commands",
"should_escape": false
},
{
"command": "/version",
"description": "Show cc-connect version",
"should_escape": false
},
{
"command": "/shell",
"description": "Run a shell command and return the output",
"usage_hint": "<command>",
"should_escape": false
},
{
"command": "/provider",
"description": "Manage API providers",
"usage_hint": "[list|add|remove|switch|clear]",
"should_escape": false
},
{
"command": "/memory",
"description": "View or edit agent memory files",
"usage_hint": "[add|global|global add]",
"should_escape": false
},
{
"command": "/allow",
"description": "Pre-allow a tool for next session",
"usage_hint": "<tool>",
"should_escape": false
},
{
"command": "/lang",
"description": "View or switch language",
"usage_hint": "[en|zh|zh-TW|ja|es|auto]",
"should_escape": false
},
{
"command": "/cron",
"description": "Manage scheduled tasks",
"usage_hint": "[add|list|del|enable|disable]",
"should_escape": false
},
{
"command": "/commands",
"description": "Manage custom slash commands",
"usage_hint": "[add|del]",
"should_escape": false
},
{
"command": "/alias",
"description": "Manage command aliases",
"usage_hint": "[add|del]",
"should_escape": false
},
{
"command": "/skills",
"description": "List agent skills",
"should_escape": false
},
{
"command": "/config",
"description": "View or update runtime configuration",
"usage_hint": "[get|set|reload] [key] [value]",
"should_escape": false
},
{
"command": "/doctor",
"description": "Run system diagnostics",
"should_escape": false
},
{
"command": "/upgrade",
"description": "Check for updates and self-update",
"should_escape": false
},
{
"command": "/restart",
"description": "Restart cc-connect service",
"should_escape": false
},
{
"command": "/reasoning",
"description": "View or switch reasoning effort level",
"usage_hint": "[low|medium|high]",
"should_escape": false
},
{
"command": "/bind",
"description": "Manage bot-to-bot relay bindings",
"usage_hint": "[project|-project|remove]",
"should_escape": false
},
{
"command": "/workspace",
"description": "Manage workspaces",
"usage_hint": "[list|switch|add|remove]",
"should_escape": false
}
]
},
"oauth_config": {
"scopes": {
"bot": [
"app_mentions:read",
"channels:history",
"channels:read",
"chat:write",
"commands",
"groups:history",
"groups:read",
"im:history",
"im:read",
"im:write",
"reactions:write",
"users:read"
]
}
},
"settings": {
"event_subscriptions": {
"bot_events": [
"app_mention",
"message.im"
]
},
"interactivity": {
"is_enabled": true
},
"org_deploy_enabled": false,
"socket_mode_enabled": true,
"token_rotation_enabled": false
}
}
+78
View File
@@ -0,0 +1,78 @@
# Slack Platform Feature Inventory
## What Existed Before Our Work (on main)
The Slack platform was added in commit `eaec71f` with basic functionality:
- **Message handling**: Direct messages only (`*slackevents.MessageEvent`)
- **File attachments**: Image and audio download via `downloadSlackFile()`
- **Threading**: Reply contexts capture channel + timestamp for threaded replies
- **Socket Mode**: Connection via `app_token` + `bot_token`
- **Session keys**: Format `slack:channel:user`
- **Methods**: `New()`, `Start()`, `Reply()`, `Send()`, `Stop()`, `ReconstructReplyCtx()`
## What We Added (feat/multi-workspace branch)
### 1. App Mention Support
- Handle `@bot` mentions in channels (`AppMentionEvent`)
- `stripAppMentionText()` helper to extract clean message text
- Commits: `ef37f6f`, `abc46cf`, `33cf135`
### 2. Slash Command Support
- Handle `socketmode.EventTypeSlashCommand` events
- Converts Slack `/command` to engine command format
- Enables native `/btw`, `/new`, `/stop`, etc. from Slack
- Commits: `81c6aec`, `2bd8518`
### 3. Multi-Workspace / Shared Sessions
- `share_session_in_channel` config option
- Session key can be channel-only (`slack:channel`) or user-scoped (`slack:channel:user`)
- `ResolveChannelName()` via `ChannelNameResolver` interface
- Channel name caching with `sync.RWMutex`
- Commits: `647398f`, `62def03`
### 4. Typing Indicator (Emoji Reactions)
- `StartTyping()` adds progressive emoji reactions to user's message
- Timeline: immediately eyes, after 2min clock, then every 5min random emoji
- All reactions cleaned up when agent completes
- Commit: `231883c`
### 5. Security
- `allow_from` config option with `core.CheckAllowFrom()` validation
- Token redaction in error messages
- Old message filtering via `core.IsOldMessage()`
- Commits: `ae13e23`, `90f0e22`
### 6. Slack mrkdwn Formatting
- System prompt instructs agent to use Slack's mrkdwn format (not standard Markdown)
- `*bold*` instead of `**bold**`, no `## headings`, etc.
- Commit: `b4a1144`
## Uncommitted Work (stashed)
### Context % Fix
- SDK reports garbage `input_tokens` (single digits like 3, 22)
- When SDK tokens < 100, falls back to agent's self-reported `[ctx: ~XX%]`
- Previously: self-reported value was always stripped and replaced with broken SDK value
### --continue on First Connection (hasConnectedOnce)
- On first session creation after engine startup, always uses `--continue`
- Picks up most recent CLI session regardless of what's stored in session manager
- Bridges direct CLI usage and cc-connect sessions
- `hasConnectedOnce` atomic.Bool prevents subsequent connections from using --continue
## Current Configuration Options
| Option | Required | Description |
|--------|----------|-------------|
| `bot_token` | Yes | Slack bot OAuth token |
| `app_token` | Yes | Slack app-level token for Socket Mode |
| `allow_from` | No | User allowlist |
| `share_session_in_channel` | No | Share session across all users in channel |
## Architecture Compliance
All Slack-specific code lives in `platform/slack/`. Core uses interface-based capability checks:
- `ChannelNameResolver` for channel name lookup
- `StartTyping()` via `TypingIndicator` interface (if implemented)
- No hardcoded "slack" references in core/
+326
View File
@@ -0,0 +1,326 @@
# Slack Setup Guide
This guide walks you through connecting **cc-connect** to Slack, so you can chat with your local Claude Code via a Slack bot.
## Prerequisites
- A Slack workspace account (with permission to create apps)
- A machine that can run cc-connect (no public IP needed)
- Claude Code installed and configured
> 💡 **Advantage**: Uses Socket Mode (WebSocket) — no public IP, no domain, no reverse proxy needed.
---
## Step 1: Create a Slack App
### 1.1 Open the Slack API Console
Go to [Slack API](https://api.slack.com/apps) and sign in with your Slack account.
### 1.2 Create a New App
1. Click "Create New App"
2. Select "From scratch"
3. Fill in the app details:
| Field | Suggested Value |
|-------|----------------|
| App Name | `cc-connect` |
| Development Slack Workspace | Select your workspace |
4. Click "Create App"
---
## Step 2: Configure Bot User
### 2.1 Go to App Home
In the left sidebar, click "App Home".
### 2.2 Set Bot Info
1. Click "Edit" to configure the bot display name
2. Fill in:
| Field | Suggested Value |
|-------|----------------|
| Display Name (Bot Name) | `cc-connect` |
| Default Username | `cc_connect` |
### 2.3 Always Show Bot Online
Toggle on "Always Show My Bot as Online".
---
## Step 3: Configure Permissions (OAuth Scopes)
### 3.1 Go to OAuth & Permissions
In the left sidebar, click "OAuth & Permissions".
### 3.2 Add Bot Token Scopes
Under "Scopes" → "Bot Token Scopes", add:
| Scope | Purpose |
|-------|---------|
| `app_mentions:read` | Read @mention messages |
| `chat:write` | Send messages |
| `im:history` | Read DM history |
| `im:read` | Read DM list |
| `im:write` | Send DMs |
| `channels:history` | Read channel messages (optional) |
| `groups:history` | Read private channel messages (optional) |
| `users:read` | Get user info |
---
## Step 4: Enable Socket Mode
### 4.1 Go to Socket Mode Settings
In the left sidebar, click "Socket Mode".
### 4.2 Enable Socket Mode
1. Toggle on "Enable Socket Mode"
2. Click "Generate Token and Enter Socket Mode"
### 4.3 Generate App-Level Token
1. Enter a token name (e.g. `cc-connect-socket-token`)
2. Add the following scope:
- `connections:write` — establish WebSocket connections
3. Click "Generate"
### 4.4 Save the Token
The system will generate an App-Level Token (format: `xapp-xxxxxxx...`). Save it immediately.
> ⚠️ The token is only shown once — copy it now!
---
## Step 5: Configure Event Subscriptions
### 5.1 Go to Event Subscriptions
In the left sidebar, click "Event Subscriptions".
### 5.2 Enable Events
1. Toggle on "Enable Events"
2. Since we're using Socket Mode, no Request URL is needed
### 5.3 Subscribe to Bot Events
Under "Subscribe to bot events", add:
| Event | Purpose |
|-------|---------|
| `app_mention` | Triggered when the bot is @mentioned |
| `message.im` | Triggered when a DM is received |
### 5.4 Save Changes
Click "Save Changes".
---
## Step 6: Install App to Workspace
### 6.1 Install the App
In the left sidebar, click "Install App" → "Install to Workspace".
### 6.2 Authorize
Review the permissions and click "Allow".
### 6.3 Get the Bot Token
After installation, you'll see:
```
Bot User OAuth Token: xoxb-xxxxxxx...
```
> ⚠️ Save this token — you'll need it for configuration.
---
## Step 7: Configure cc-connect
Add both tokens to your `config.toml`:
```toml
[[projects]]
name = "my-project"
[projects.agent]
type = "claudecode"
[projects.agent.options]
work_dir = "/path/to/your/project"
mode = "default"
[[projects.platforms]]
type = "slack"
[projects.platforms.options]
bot_token = "xoxb-xxxxxxx..."
app_token = "xapp-xxxxxxx..."
```
### Token Reference
| Token | Prefix | Purpose |
|-------|--------|---------|
| Bot Token | `xoxb-` | Bot API authentication |
| App Token | `xapp-` | Socket Mode connection |
---
## Step 8: Start cc-connect
### 8.1 Launch
```bash
cc-connect
# Or specify a config file
cc-connect -config /path/to/config.toml
```
### 8.2 Verify Connection
You should see logs like:
```
level=INFO msg="slack: connected"
level=INFO msg="platform started" project=my-project platform=slack
level=INFO msg="cc-connect is running" projects=1
```
---
## Step 9: Start Chatting
### 9.1 Direct Message
1. Search for your bot name in Slack
2. Open a DM conversation
3. Send a message
### 9.2 Channel Usage
1. Add the bot to a channel (`/invite @cc_connect`)
2. @mention the bot: `@cc_connect help me analyze the code`
3. The bot will respond
---
## Usage Example
```
User: @cc_connect Help me analyze the current project structure
cc-connect: 🤔 Thinking...
cc-connect: 🔧 Tool: Bash(ls -la)
cc-connect: Here's the project structure...
```
---
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Slack Cloud │
│ │
│ User Message ──→ Slack API ──→ Socket Mode Gateway │
│ │ │
└───────────────────────────────────────┼──────────────────────┘
│ WebSocket (no public IP needed)
┌─────────────────────────────────────────────────────────────┐
│ Your Local Machine │
│ │
│ cc-connect ◄──► Claude Code CLI ◄──► Your Project Code │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Socket Mode vs Webhook
| Feature | Socket Mode | Webhook |
|---------|-------------|---------|
| Public IP | ❌ Not needed | ✅ Required |
| Domain | ❌ Not needed | ✅ Required |
| HTTPS cert | ❌ Not needed | ✅ Required |
| Reverse proxy | ❌ Not needed | ✅ Required |
| Connection | WebSocket | HTTP callback |
| Complexity | Simple | More complex |
| Best for | Local dev, private network | Production |
---
## FAQ
### Q: Socket Mode connection fails?
Check the following:
1. Is the App Token correct? (starts with `xapp-`)
2. Does the App Token have `connections:write` scope?
3. Is Socket Mode enabled in the app settings?
### Q: Bot doesn't respond to messages?
Check the following:
1. Is the Bot Token correct? (starts with `xoxb-`)
2. Are event subscriptions configured correctly?
3. Are the required scopes added?
### Q: Changes to permissions don't take effect?
**⚠️ Important**: After modifying scopes or events, you must reinstall the app!
1. Go to "Install App"
2. Click "Reinstall to Workspace"
### Q: Bot doesn't respond in DMs?
Make sure you've subscribed to the `message.im` event.
### Q: Bot doesn't respond in channels?
Make sure:
1. You've subscribed to the `app_mention` event
2. The bot has been added to the channel
3. You @mentioned the bot in your message
---
## References
- [Slack API Documentation](https://api.slack.com/)
- [Slack App Building Guide](https://api.slack.com/start/building)
- [Socket Mode Documentation](https://api.slack.com/apis/connections/socket)
- [Bot Token Scopes](https://api.slack.com/scopes)
- [Event Types](https://api.slack.com/events)
---
## See Also
- [Feishu Setup](./feishu.md)
- [DingTalk Setup](./dingtalk.md)
- [Weibo Setup](./weibo.md)
- [Telegram Setup](./telegram.md)
- [Discord Setup](./discord.md)
- [Back to README](../README.md)
+280
View File
@@ -0,0 +1,280 @@
# Telegram Setup Guide
This guide walks you through connecting **cc-connect** to Telegram, so you can chat with your local Claude Code via a Telegram bot.
## Prerequisites
- A Telegram account
- A machine that can run cc-connect (no public IP needed)
- Claude Code installed and configured
> 💡 **Advantage**: Uses Long Polling mode — no public IP, no domain, no reverse proxy needed.
---
## Step 1: Create a Telegram Bot
### 1.1 Open BotFather
Search for **@BotFather** in Telegram (the official bot manager) and start a chat.
> ⚠️ Make sure it's the verified official BotFather — don't use third-party imitations.
### 1.2 Create a New Bot
Send the command `/newbot`. BotFather will ask you to provide a name and username.
### 1.3 Set the Bot Name
Enter a **display name** for your bot (e.g. `cc-connect`).
### 1.4 Set the Bot Username
Enter a **username** (must end with `bot`, e.g. `cc_connect_bot`).
> 💡 **Naming rules:**
> - Must end with `bot` (case-insensitive)
> - Only letters, numbers, and underscores
> - Must be globally unique
### 1.5 Get the Bot Token
After creation, BotFather will reply with something like:
```
Done! Congratulations on your new bot...
Use this token to access the HTTP API:
1234567890:ABCdefGHIjklMNOpqrsTUVwxyz-123456
Keep your token secure...
```
> ⚠️ Save this token immediately — it's only shown once! If lost, use `/mybots` → select bot → `API Token` → `Revoke current token` to regenerate.
---
## Step 2: Configure cc-connect
Add the token to your `config.toml`:
```toml
[[projects]]
name = "my-project"
# ── Project-level settings ──────────────────────────────────
# admin_from: who can run privileged commands (/shell, /restart, /upgrade).
# Not set (default) → privileged commands are blocked for everyone.
# "*" → all allowed users get admin access (only for personal single-user setups).
# "id1,id2" → only these Telegram user IDs can run privileged commands.
admin_from = "*"
[projects.agent]
type = "claudecode"
[projects.agent.options]
work_dir = "/path/to/your/project"
mode = "default"
[[projects.platforms]]
type = "telegram"
[projects.platforms.options]
token = "1234567890:ABCdefGHIjklMNOpqrsTUVwxyz-123456"
# ── Platform-level settings ─────────────────────────────────
# allow_from: who can use this bot.
# Not set (default) → all users are permitted (a WARN will be logged).
# "*" → same as not set, but explicit (no WARN).
# "id1,id2" → only these Telegram user IDs can interact with the bot.
# allow_from = "123456789"
```
> **Common mistake:** `admin_from` goes under `[[projects]]` (project level), NOT inside `[projects.platforms.options]`. If placed in the wrong section, it will be silently ignored.
>
> To find your Telegram user ID, send any message to **@userinfobot**.
---
## Step 3: Get Chat ID (Optional)
If you want to restrict the bot to specific users/groups, you'll need the Chat ID.
### 3.1 Get Your Personal Chat ID
1. Send any message to your bot
2. Visit the following URL (replace `{{TOKEN}}` with your token):
```
https://api.telegram.org/bot{{TOKEN}}/getUpdates
```
3. Find the `chat.id` field in the returned JSON
### 3.2 Get a Group Chat ID
1. Add the bot to a group
2. Send a message mentioning @your_bot in the group
3. Check the `getUpdates` URL — group Chat IDs are usually negative numbers
> Note: Chat ID whitelisting is planned for a future release.
---
## Step 4: Set Bot Commands (Optional)
### 4.1 Set Command Menu
In BotFather, send:
```
/setcommands
```
Select your bot, then enter the command list:
```
help - Show available commands
new - Start a new session
list - List sessions
```
### 4.2 Set Bot Description
```
/setdescription
```
Enter a description — users will see this when they first open the bot.
---
## Step 5: Start cc-connect
### 5.1 Launch
```bash
cc-connect
# Or specify a config file
cc-connect -config /path/to/config.toml
```
### 5.2 Verify Connection
You should see logs like:
```
level=INFO msg="telegram: connected" bot=cc_connect_bot
level=INFO msg="platform started" project=my-project platform=telegram
level=INFO msg="cc-connect is running" projects=1
```
---
## Step 6: Start Chatting
### 6.1 Direct Message
1. Search for your bot's username in Telegram
2. Click "Start" to begin
3. Send a message
### 6.2 Group Chat
1. Create or open a group
2. Go to group settings → Add members
3. Search and add your bot
4. Send messages in the group
### 6.3 Topic Sessions
Telegram topics include a `message_thread_id`. cc-connect uses that thread ID
as part of the Telegram session key, so each topic has its own independent
conversation context. This applies to forum topics in groups and private chat
topics when Telegram includes `message_thread_id`.
---
## Usage Example
```
User: Help me analyze the current project structure
cc-connect: 🤔 Thinking...
cc-connect: 🔧 Tool: Bash(ls -la)
cc-connect: Here's the project structure...
```
---
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Telegram Cloud │
│ │
│ User Message ──→ Telegram Bot API ◄── Long Polling │
│ ▲ │
└──────────────────────────┼───────────────────────────────────┘
│ HTTPS (no public IP needed)
┌─────────────────────────────────────────────────────────────┐
│ Your Local Machine │
│ │
│ cc-connect ◄──► Claude Code CLI ◄──► Your Project Code │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Long Polling vs Webhook
| Feature | Long Polling | Webhook |
|---------|-------------|---------|
| Public IP | ❌ Not needed | ✅ Required |
| Domain | ❌ Not needed | ✅ Required |
| HTTPS cert | ❌ Not needed | ✅ Required |
| Complexity | Simple | More complex |
| Latency | Low (long poll) | Low |
| Best for | Local dev, private network | Production |
---
## FAQ
### Q: Bot doesn't respond to messages?
Check the following:
1. Is cc-connect running?
2. Is the bot token correct?
3. Have you sent a message after starting cc-connect? (The bot only receives messages after startup)
### Q: How to regenerate the token?
1. Send `/mybots` to BotFather
2. Select your bot
3. Click `API Token``Revoke current token`
### Q: Bot doesn't respond in groups?
Make sure Group Privacy mode is disabled. In BotFather: `/mybots` → select bot → `Bot Settings``Group Privacy``Turn off`.
---
## References
- [Telegram Bot API Documentation](https://core.telegram.org/bots/api)
- [BotFather Guide](https://core.telegram.org/bots#botfather)
- [Telegram Bot Tutorial](https://core.telegram.org/bots/tutorial)
---
## See Also
- [Feishu Setup](./feishu.md)
- [DingTalk Setup](./dingtalk.md)
- [Weibo Setup](./weibo.md)
- [Slack Setup](./slack.md)
- [Discord Setup](./discord.md)
- [Back to README](../README.md)
+1023
View File
File diff suppressed because it is too large Load Diff
+935
View File
@@ -0,0 +1,935 @@
# 使用指南
cc-connect 完整功能使用指南。
## 目录
- [会话管理](#会话管理)
- [权限模式](#权限模式)
- [API Provider 管理](#api-provider-管理)
- [模型选择](#模型选择)
- [工作目录切换(`/dir`、`/cd`](#工作目录切换dircd)
- [引用查看(`/show`](#引用查看show)
- [飞书配置 CLI](#飞书配置-cli)
- [微信个人号配置 CLI](#微信个人号配置-cli)
- [Claude Code Router 集成](#claude-code-router-集成)
- [语音消息(语音转文字)](#语音消息语音转文字)
- [语音回复(文字转语音)](#语音回复文字转语音)
- [图片与文件回传](#图片与文件回传)
- [定时任务 (Cron)](#定时任务-cron)
- [多机器人中继](#多机器人中继)
- [守护进程模式](#守护进程模式)
- [多工作区模式](#多工作区模式)
- [Web 管理后台(Beta](#web-管理后台beta)
- [Bridge — 外部适配器接入(Beta)](#bridge--外部适配器接入beta)
- [配置参考](#配置参考)
---
## 会话管理
每个用户拥有独立的会话和完整的对话上下文。通过斜杠命令管理:
| 命令 | 说明 |
|------|------|
| `/new [名称]` | 创建新会话 |
| `/list` | 列出当前项目的会话 |
| `/switch <id>` | 切换到指定会话 |
| `/current` | 查看当前会话 |
| `/history [n]` | 查看最近 n 条消息 |
| `/usage` | 查看账号/模型限额使用情况 |
| `/provider [...]` | 管理 API Provider |
| `/model [switch <alias>]` | 列出可用模型或按别名切换 |
| `/dir [路径]` | 查看或切换 Agent 工作目录 |
| `/show <引用>` | 按引用查看文件、目录或代码片段 |
| `/allow <工具名>` | 预授权工具 |
| `/reasoning [等级]` | 查看或切换推理强度(Codex)|
| `/mode [名称]` | 查看或切换权限模式 |
| `/stop` | 停止当前执行 |
| `/help` | 显示可用命令 |
会话中 Agent 请求工具权限时,回复 **允许** / **拒绝** / **允许所有**
也可以为项目开启“空闲后自动切换新会话”:
```toml
[[projects]]
name = "demo"
reset_on_idle_mins = 60
```
开启后,如果用户长时间未发消息,下一条普通消息会自动进入一个新的会话;旧会话仍会保留在 `/list` 中,不会被删除。
### 切换模型时保留历史
`/model` 切换模型时保留当前会话——agent 会在新模型下继续对话(不额外消耗 token)。注意模型切换作用于共享的 agent 实例——如果多个平台使用同一个 project,模型变更会影响所有平台。
---
## 权限模式
所有 Agent 支持运行时切换权限模式,通过 `/mode` 命令。
### Claude Code 模式
| 模式 | 配置值 | 行为 |
|------|--------|------|
| 默认 | `default` | 每次工具调用需确认 |
| 接受编辑 | `acceptEdits` / `edit` | 文件编辑自动通过 |
| 自动模式 | `auto` | 由 Claude 自动判断何时需要确认 |
| 计划模式 | `plan` | 只规划不执行 |
| YOLO | `bypassPermissions` / `yolo` | 全部自动通过 |
### Codex 模式
| 模式 | 配置值 | 行为 |
|------|--------|------|
| 建议 | `suggest` | 仅受信命令自动执行 |
| 自动编辑 | `auto-edit` | 模型自行决定 |
| 全自动 | `full-auto` | 自动通过 + 沙箱保护 |
| YOLO | `yolo` | 跳过所有审批 |
### Cursor Agent 模式
| 模式 | 配置值 | 行为 |
|------|--------|------|
| 默认 | `default` | 工具调用前询问 |
| 强制执行 | `force` / `yolo` | 自动批准所有 |
| 规划模式 | `plan` | 只读分析 |
| 问答模式 | `ask` | 问答风格,只读 |
### Gemini CLI 模式
| 模式 | 配置值 | 行为 |
|------|--------|------|
| 默认 | `default` | 每次需确认 |
| 自动编辑 | `auto_edit` / `edit` | 编辑自动通过 |
| 全自动 | `yolo` | 自动批准所有 |
| 规划模式 | `plan` | 只读规划 |
### Qoder CLI / OpenCode / iFlow CLI
| 模式 | 配置值 | 行为 |
|------|--------|------|
| 默认 | `default` | 标准权限 |
| YOLO | `yolo` | 跳过所有检查 |
### 配置示例
```toml
[projects.agent.options]
mode = "default"
# allowed_tools = ["Read", "Grep", "Glob"]
```
运行时切换:
```
/mode # 查看当前和可用模式
/mode yolo # 切换到 YOLO 模式
/mode default # 切回默认
```
---
## API Provider 管理
运行时切换 API Provider,无需重启。
### 配置 Provider
```toml
[projects.agent.options]
work_dir = "/path/to/project"
provider = "anthropic"
[[projects.agent.providers]]
name = "anthropic"
api_key = "sk-ant-xxx"
[[projects.agent.providers]]
name = "relay"
api_key = "sk-xxx"
base_url = "https://api.relay-service.com"
model = "claude-sonnet-4-20250514"
[[projects.agent.providers.models]]
model = "claude-sonnet-4-20250514"
alias = "sonnet"
[[projects.agent.providers.models]]
model = "claude-opus-4-20250514"
alias = "opus"
[[projects.agent.providers.models]]
model = "claude-haiku-3-5-20241022"
alias = "haiku"
# MiniMax — 兼容 OpenAI 接口,1M 超长上下文
[[projects.agent.providers]]
name = "minimax"
api_key = "your-minimax-api-key"
base_url = "https://api.minimax.io/v1"
model = "MiniMax-M2.7"
# Bedrock、Vertex 等
[[projects.agent.providers]]
name = "bedrock"
env = { CLAUDE_CODE_USE_BEDROCK = "1", AWS_PROFILE = "bedrock" }
```
### CLI 命令
```bash
cc-connect provider add --project my-backend --name relay --api-key sk-xxx --base-url https://api.relay.com
cc-connect provider list --project my-backend
cc-connect provider remove --project my-backend --name relay
cc-connect provider import --project my-backend # 从 cc-switch 导入
```
### 聊天命令
```
/provider 查看当前 Provider
/provider list 列出所有
/provider add <名称> <key> [url] [model]
/provider remove <名称>
/provider switch <名称>
/provider <名称> 切换快捷方式
```
### 环境变量映射
| Agent | api_key → | base_url → |
|-------|-----------|------------|
| Claude Code | `ANTHROPIC_API_KEY` | `ANTHROPIC_BASE_URL` |
| Codex | `OPENAI_API_KEY` | `OPENAI_BASE_URL` |
| Gemini CLI | `GEMINI_API_KEY` | 使用 `env` 字段 |
| OpenCode | `ANTHROPIC_API_KEY` | 使用 `env` 字段 |
| iFlow CLI | `IFLOW_API_KEY` | `IFLOW_BASE_URL` |
---
## 模型选择
通过 `[[providers.models]]` 为每个 Provider 预配置可选模型列表。每个条目包含 `model`(模型标识符)和可选的 `alias`(别名,显示在 `/model` 中)。
### 配置模型
```toml
[[projects.agent.providers]]
name = "openai"
api_key = "sk-xxx"
[[projects.agent.providers.models]]
model = "gpt-5.3-codex"
alias = "codex"
[[projects.agent.providers.models]]
model = "gpt-5.4"
alias = "gpt"
[[projects.agent.providers.models]]
model = "gpt-5.3-codex-spark"
alias = "spark"
```
### 聊天命令
```
/model 列出可用模型(格式:alias - model
/model switch <alias> 按别名切换模型
/model switch <name> 按完整名称切换模型
/model <alias> 兼容旧写法,仍然可用
```
配置了 `models` 时,`/model` 直接显示该列表,不发起 API 请求。未配置时,自动从 Provider API 获取或使用内置备选列表。
---
## 工作目录切换(`/dir``/cd`
可直接在聊天中切换 Agent 下一次会话的工作目录。
### 聊天命令
```
/dir 查看当前工作目录和最近历史
/dir <路径> 切换到指定路径(相对或绝对)
/dir <序号> 按历史序号切换目录
/dir - 返回上一个目录
/dir help 查看命令用法
/cd <路径> `/dir <路径>` 的兼容别名
```
### 行为说明
- 目录切换会作用于当前项目的下一次会话。
- 相对路径基于当前 Agent 工作目录解析。
- 目录历史按项目隔离,可通过序号快速切换。
- `/cd` 为兼容保留,建议优先使用 `/dir`
示例:
```text
/dir ../another-repo
/dir 2
/dir -
```
---
## 本地引用展示配置(`[projects.references]`
可选启用对 Agent 输出中的本地文件 / 目录 / 代码位置引用进行标准化与重渲染,提升在 IM 平台中的可读性。
这是一个 **opt-in** 功能:
- 未配置 `[projects.references]` 时,现有行为保持不变
- 只有命中 `normalize_agents``render_platforms` 时,才会启用
### 推荐配置
```toml
[projects.references]
normalize_agents = ["all"]
render_platforms = ["all"]
display_path = "relative"
marker_style = "emoji"
enclosure_style = "code"
```
### 字段说明
- `normalize_agents`
- 控制哪些 Agent 输出参与这套引用处理
- 当前初始支持:`codex``claudecode``all`
- `render_platforms`
- 控制在哪些平台发送前应用展示重写
- 当前初始支持:`feishu``weixin``all`
- `display_path`
- 控制路径主体的显示层级
- 可选值:`absolute``relative``basename``dirname_basename``smart`
- `marker_style`
- 控制前缀标记样式
- 可选值:`none``ascii``emoji`
- `enclosure_style`
- 控制路径主体的包裹样式
- 可选值:`none``bracket``angle``fullwidth``code`
### 支持的引用输入
当前初始支持识别这些常见形式:
- 绝对路径
- 相对路径
- 文件 / 目录引用
- `path:line`
- `path:line:col`
- `path:start-end`
- `path#L42`
- Markdown 本地文件链接
- Claude 风格的反引号绝对路径引用
### 行为说明
- 只处理 Agent 输出:
- thinking
- final response
- stream preview
- progress / card 中的 Agent 文本
- 不处理:
- 系统消息
- `/workspace``/dir``/status` 等命令回复
- raw tool result
- 网页链接会保持原样,不会被本地引用重写逻辑污染
### 推荐默认值说明
当前最推荐的组合是:
- `display_path = "relative"`
- `marker_style = "emoji"`
- `enclosure_style = "code"`
这样通常会得到类似:
- `📄 ui/recovery_contact_form.tsx:11`
- `📁 docs/spec.v1/`
如果不希望使用 emoji,更推荐:
- `display_path = "dirname_basename"`
- `marker_style = "ascii"`
- `enclosure_style = "code"`
---
## 引用查看(`/show`
可直接基于一个文件 / 目录 / 代码位置引用查看内容,而不必手写 `/shell sed ...`
### 聊天命令
```text
/show <路径> 查看文件前 80 行
/show <路径:行号> 查看该行附近上下文
/show <路径:起止行> 查看指定 range
/show <目录路径/> 查看一级目录列表
```
支持的输入形式包括:
- 绝对路径
- 相对路径(相对当前 Agent 工作目录)
- `path:line`
- `path:line:col`
- `path:start-end`
- `path#L42`
- Markdown 本地文件链接,如:
- `[file.ts](/abs/path/file.ts#L42)`
### 行为说明
- 文件,无位置:
- 默认显示文件前 80 行
- `path:line` / `path#L42`
- 默认显示该位置附近上下文
- `path:start-end`
- 默认显示该 range
- 目录:
- 默认显示一级目录内容
说明:
- `/show` 只解析“纯引用文本”,不解析前端展示层包装后的 `📄 ...` / `[FILE] ...` 这类样式
- `/show` 属于本地文件系统查看命令,与 `/shell``/dir` 类似,默认受 `admin_from` 权限控制
- 执行 Shell 命令支持 `!` 快捷前缀:`!ls -la` 等同于 `/shell ls -la``! --timeout 300 npm install` 可指定超时时间
示例:
```text
/show ui/recovery_contact_form.tsx
/show svc/recovery_session_reconciler.go:12
/show svc/recovery_session_reconciler_test.go:8-17
/show docs/spec.v1/
```
---
## 飞书配置 CLI
可以直接通过 CLI 完成飞书/Lark 机器人创建或关联,并自动写回 `config.toml`
```bash
# 推荐:统一入口
cc-connect feishu setup --project my-project
cc-connect feishu setup --project my-project --app cli_xxx:sec_xxx
# 强制模式(一般不需要)
cc-connect feishu new --project my-project
cc-connect feishu bind --project my-project --app cli_xxx:sec_xxx
```
区别说明:
- `setup`:统一入口。没传凭证时等价 `new`,传了 `--app` 时等价 `bind`
- `new`:强制二维码新建,不接受 `--app`
- `bind`:强制关联已有机器人,必须提供凭证。
行为说明(通用):
- `setup` 默认走二维码新建;传入 `--app` 时自动切换到关联已有机器人。
- `--project` 不存在会自动创建。
- 项目存在但没有 `feishu/lark` 平台时会自动补一个平台配置。
- 命令会回填凭证(`app_id` / `app_secret`);扫码新建场景下飞书通常会预配权限和事件订阅。
- 建议在飞书开放平台再核验一次发布状态与可用范围。
- 运行时平台配置还支持可选 `domain` 覆盖 Feishu/Lark API 域名;这不会改变 `setup/new/bind` 的引导地址。
---
## 微信个人号配置 CLI
个人微信走 **ilink 机器人网关**HTTP 长轮询,与 OpenClaw `openclaw-weixin` 同类)。可直接用 CLI 扫码登录或绑定已有 Token,并写回 `config.toml`
**完整图文流程与配置项说明见:[docs/weixin.md](./weixin.md)。**
```bash
# 推荐:终端展示二维码 + URL,微信扫码确认后自动写配置
cc-connect weixin setup --project my-project
# 已有 Bearer Token(例如从 OpenClaw 导出)
cc-connect weixin bind --project my-project --token '<token>'
cc-connect weixin setup --project my-project --token '<token>'
# 强制只走扫码(不接受 --token)
cc-connect weixin new --project my-project
```
区别说明:
- `setup`:未传 `--token` 时走扫码;传了 `--token` 时等同绑定并可选校验。
- `new`:强制扫码。
- `bind`:强制绑定,必须 `--token`
行为说明:
- `--project` 不存在时会自动创建项目;项目里没有 `weixin` 平台时会自动追加一块 `[[projects.platforms]]`
- 扫码成功后会写入 `token`,以及网关返回的 `base_url`(若有)、`ilink_bot_id``account_id` 等。
- 默认 `--set-allow-from-empty=true`:若 `allow_from` 为空,会用扫码用户的 ilink ID 预填,便于收紧权限。
- 绑定时默认调用 `getUpdates` 校验 Token;可用 `--skip-verify` 跳过。
- 首次使用后请在微信里 **先发一条消息**,以便缓存 `context_token`,否则可能无法回复。
常用参数:`--api-url``--cdn-url``--timeout``--qr-image``--route-tag``--bot-type``--debug`(详见 `cc-connect weixin help` 或 [weixin.md](./weixin.md))。
---
## Claude Code Router 集成
[Claude Code Router](https://github.com/musistudio/claude-code-router) 可将请求路由到不同模型提供商。
### 安装配置
1. 安装:`npm install -g @musistudio/claude-code-router`
2. 配置 `~/.claude-code-router/config.json`
```json
{
"APIKEY": "your-secret-key",
"Providers": [
{
"name": "deepseek",
"api_base_url": "https://api.deepseek.com/chat/completions",
"api_key": "sk-xxx",
"models": ["deepseek-chat", "deepseek-reasoner"],
"transformer": { "use": ["deepseek"] }
}
],
"Router": {
"default": "deepseek,deepseek-chat",
"think": "deepseek,deepseek-reasoner"
}
}
```
3. 启动:`ccr start`
4. 配置 cc-connect
```toml
[projects.agent.options]
router_url = "http://127.0.0.1:3456"
router_api_key = "your-secret-key"
```
---
## 语音消息(语音转文字)
发送语音消息,自动转文字。
**支持平台:** 飞书、企业微信、Telegram、LINE、Discord、Slack
**前置条件:** OpenAI/Groq API Key`ffmpeg`
### 配置
```toml
[speech]
enabled = true
provider = "openai" # 或 "groq"
language = "" # "zh"、"en" 或留空自动检测
[speech.openai]
api_key = "sk-xxx"
# [speech.groq]
# api_key = "gsk_xxx"
# model = "whisper-large-v3-turbo"
```
### 安装 ffmpeg
```bash
# Ubuntu/Debian
sudo apt install ffmpeg
# macOS
brew install ffmpeg
```
---
## 语音回复(文字转语音)
将 AI 回复合成语音发送。
**支持平台:** 飞书
### 配置
```toml
[tts]
enabled = true
provider = "qwen" # 或 "openai"
voice = "Cherry"
tts_mode = "voice_only" # "voice_only" | "always"
max_text_len = 0
[tts.qwen]
api_key = "sk-xxx"
```
### TTS 模式
| 模式 | 行为 |
|------|------|
| `voice_only` | 仅当用户发语音时才语音回复 |
| `always` | 始终语音回复 |
切换:`/tts always``/tts voice_only`
---
## 图片与文件回传
当 Agent 在本地生成了图片、PDF、日志包、报表等文件,需要把结果直接发回当前聊天时,可以使用 `cc-connect send` 的附件模式。
**当前支持平台:**
- 飞书
- Telegram
### 什么时候需要先执行 setup
如果当前 Agent 不是“原生 system prompt 注入”类型,升级到包含该功能的版本后,建议先在聊天里执行一次:
```text
/bind setup
```
或者:
```text
/cron setup
```
这两个命令写入的是同一份 cc-connect 指令。执行任意一个即可。这样 Agent 才会知道:
- 普通文本回复直接正常输出
- 生成附件后用 `cc-connect send --image/--file` 回传
如果你以前已经执行过 setup,也建议升级后重新执行一次,以刷新到最新指令。
### 配置开关
如果你想禁用 agent 主动回传附件,可以在 `config.toml` 里加入:
```toml
attachment_send = "off"
```
默认值是 `on`。这个开关与 agent 的 `/mode` 独立,只影响 `cc-connect send --image/--file` 这条图片/文件回传路径。
### CLI 用法
```bash
cc-connect send --image /absolute/path/to/chart.png
cc-connect send --file /absolute/path/to/report.pdf
cc-connect send --file /absolute/path/to/report.pdf --image /absolute/path/to/chart.png
```
说明:
- `--image` 用于图片附件。
- `--file` 用于任意文件附件。
- `--message` 可选,用于先发一段说明文字,再发附件。
- `--image``--file` 都可以重复多次。
- 建议使用绝对路径,避免 Agent 当前工作目录变化导致找不到文件。
- 如果设置了 `attachment_send = "off"`,图片/文件回传会被拒绝,但普通文本回复仍然正常。
### 典型场景
1. Agent 生成了截图或图表,需要直接发给用户。
2. Agent 生成了 PDF、Markdown 导出、日志包或补丁文件,需要作为附件交付。
3. Agent 想告诉用户“结果已生成”,同时附上一个或多个文件。
### 注意事项
- 这个命令是给“附件回传”用的,不要拿它代替普通文本回复。
- 只能发送本机上 Agent 可访问到的文件。
- 必须存在活跃会话;如果当前项目没有活动聊天上下文,命令会失败。
- 平台本身仍可能有文件大小或文件类型限制。
---
## 定时任务 (Cron)
创建自动执行的定时任务。
### 聊天命令
```
/cron 列出所有任务
/cron add <分> <时> <日> <月> <周> <任务描述> 创建任务
/cron del <id> 删除任务
/cron enable <id> 启用
/cron disable <id> 禁用
```
示例:
```
/cron add 0 6 * * * 帮我收集 GitHub trending 并总结
```
### CLI 命令
```bash
cc-connect cron add --cron "0 6 * * *" --prompt "总结 GitHub trending" --desc "每日趋势"
cc-connect cron list
cc-connect cron edit <job-id> <field> <value> # 可改 cron_expr / prompt / enabled / mute / timeout_mins 等
cc-connect cron del <job-id>
```
可选:`--session-mode new-per-run` 每次触发使用新的 agent 会话(默认 `reuse` 与旧行为一致)。`--timeout-mins N` 设置单次调度最长等待分钟数(`0` 表示不限制;省略为 30 分钟)。
### 自然语言(Claude Code
> "每天早上6点帮我总结 GitHub trending"
Claude Code 会自动创建定时任务。对依赖记忆文件的其他 Agent,先执行一次 `/cron setup``/bind setup`,效果相同。
---
## 多机器人中继
跨平台机器人通信,群聊多机器人协作。
### 群聊绑定
```
/bind 查看绑定
/bind claudecode 添加 claudecode 项目
/bind gemini 添加 gemini 项目
/bind -claudecode 移除 claudecode
```
### 机器人间通信
```bash
cc-connect relay send --to gemini "你觉得这个架构怎么样?"
```
---
## 守护进程模式
后台服务运行。
```bash
cc-connect daemon install --config ~/.cc-connect/config.toml
cc-connect daemon start
cc-connect daemon stop
cc-connect daemon restart
cc-connect daemon status
cc-connect daemon logs [-f]
cc-connect daemon uninstall
```
---
## 多工作区模式
一个 bot 服务多个工作区,每个频道一个独立工作目录。
### 配置
```toml
[[projects]]
name = "my-project"
mode = "multi-workspace"
base_dir = "~/workspaces"
[projects.agent]
type = "claudecode"
```
### 命令
```
/workspace 查看当前绑定
/workspace bind <名称> 绑定本地文件夹
/workspace init <git-url> 克隆仓库并绑定
/workspace unbind 解除绑定
/workspace list 列出所有绑定
```
### 工作原理
- 频道名 `#project-a` → 自动绑定 `base_dir/project-a/`
- 每个频道有独立的会话和 Agent 状态
---
## Web 管理后台(Beta
> **状态:Beta。** 此功能自 v1.2.2-beta.5 起可用,UI 和 API 在后续版本中可能调整。
内嵌在二进制中的全功能管理界面,支持项目管理、会话管理、定时任务编辑、全局设置、聊天界面、多语言等。
### 快速启用(聊天命令)
最简单的方式,在聊天中发送:
```
/web setup
```
该命令会自动在 `config.toml` 中启用 **Management API****Bridge**,生成 token,并返回访问地址。首次启用后需要执行 `/restart` 使配置生效。
启用后,打开返回的地址(默认 `http://localhost:9820`),用显示的 token 登录即可。
### 查看状态
```
/web # 或 /web status — 查看 Web 管理后台的地址和启用状态
```
### 手动配置
`config.toml` 中添加:
```toml
[management]
enabled = true
port = 9820 # 管理后台监听端口
token = "your-secret-token" # 登录 token/web setup 会自动生成
cors_origins = ["*"] # 允许的 CORS 来源;留空则不设置 CORS 头
```
然后重启 cc-connect。
### 构建选项
Web 前端资源默认编译进二进制。如果想排除(减小约 1MB):
```bash
make build-noweb
# 或
go build -tags 'no_web' ./cmd/cc-connect
```
使用 `no_web` 构建时,`/web` 命令会提示 Web 管理后台不可用。
### Management API
API 与 Web UI 共用同一端口。基础 URL:`http://<host>:<port>/api/v1`
所有 API 请求需要 `Authorization: Bearer <token>` 请求头。
主要接口:
| 方法 | 路径 | 说明 |
|------|------|------|
| `GET` | `/api/v1/status` | 系统状态(版本、运行时间、已连接平台) |
| `POST` | `/api/v1/restart` | 重启 cc-connect |
| `POST` | `/api/v1/reload` | 重新加载配置 |
| `GET` | `/api/v1/projects` | 项目列表 |
| `GET` | `/api/v1/sessions?project=<name>` | 查询项目的会话列表 |
| `GET` | `/api/v1/cron` | 定时任务列表 |
| `GET` | `/api/v1/settings` | 获取全局设置 |
| `PATCH` | `/api/v1/settings` | 更新全局设置 |
完整 API 参考:[management-api.md](./management-api.md)[中文版](./management-api.zh-CN.md)
---
## Bridge — 外部适配器接入(Beta)
> **状态:Beta。** 此功能自 v1.2.2-beta.5 起可用,协议在后续版本中可能调整。
Bridge 提供 WebSocket + REST 服务,让外部适配器(自定义 UI、机器人、脚本等)可以接入 cc-connect —— 发送消息、接收 Agent 事件、管理会话。
### 通过聊天启用
`/web setup` 命令会同时启用 Bridge 和管理后台,无需额外操作。
### 手动配置
`config.toml` 中添加:
```toml
[bridge]
enabled = true
port = 9810 # Bridge 监听端口(与管理后台分开)
token = "your-bridge-secret" # WebSocket 和 REST 的认证 token
path = "/bridge/ws" # WebSocket 端点路径
cors_origins = ["*"] # 允许的 CORS 来源;留空则不设置 CORS
```
然后重启 cc-connect。
### 认证方式
所有 Bridge 连接需要 token 认证,支持三种方式:
- URL 参数:`?token=<bridge-token>`
- 请求头:`Authorization: Bearer <bridge-token>`
- 请求头:`X-Bridge-Token: <bridge-token>`
### WebSocket 接入
连接地址:
```
ws://<host>:<bridge-port>/bridge/ws?token=<bridge-token>
```
WebSocket 支持双向通信 —— 向 Agent 发送消息,并实时接收 Agent 的文本回复、工具调用、权限请求等事件。
### REST API
与 WebSocket 共用同一端口。
| 方法 | 路径 | 说明 |
|------|------|------|
| `GET` | `/bridge/sessions?session_key=...&project=...` | 查询会话列表 |
| `POST` | `/bridge/sessions` | 创建新会话 |
| `GET` | `/bridge/sessions/{id}?session_key=...&project=...` | 获取会话详情及历史 |
| `DELETE` | `/bridge/sessions/{id}?session_key=...&project=...` | 删除会话 |
| `POST` | `/bridge/sessions/switch` | 切换当前活跃会话 |
完整协议参考:[bridge-protocol.md](./bridge-protocol.md)[中文版](./bridge-protocol.zh-CN.md)
### 端口汇总
| 服务 | 默认端口 | 配置块 |
|------|---------|--------|
| 管理后台(Web UI + API | 9820 | `[management]` |
| BridgeWebSocket + REST | 9810 | `[bridge]` |
---
## 配置参考
完整配置示例见 [config.example.toml](../config.example.toml)。
### 项目结构
```toml
[[projects]]
name = "my-project"
[projects.agent]
type = "claudecode" # 或 codex, cursor, gemini, qoder, opencode, iflow
[projects.agent.options]
work_dir = "/path/to/project"
mode = "default"
provider = "anthropic"
[[projects.platforms]]
type = "feishu" # 或 wps-xiezuo, dingtalk, telegram, slack, discord, wecom, weixin, line, qq, qqbot
[projects.platforms.options]
# 平台特定配置
```
+446
View File
@@ -0,0 +1,446 @@
# 企业微信 (WeChat Work) 接入指南
本文档介绍如何将 **cc-connect** 接入企业微信,让你可以通过企业微信(甚至个人微信)远程调用 Claude Code。
> 💡 **特色功能**:配置完成后,**个人微信用户也可以直接对话** —— 只需在企业微信管理后台关联微信插件即可。
企业微信支持两种接入模式:
| 模式 | 优势 | 要求 |
|------|------|------|
| **WebSocket 长连接**(推荐) | 无需公网 URL、无需消息加解密、无需 IP 白名单,支持图片接收与图片回传 | 创建「智能机器人」 |
| **Webhook 回调** | 兼容企业微信自建应用回调模式 | 公网 URL + 可信 IP |
---
## 模式一:WebSocket 长连接(推荐)
企业微信「智能机器人」支持 WebSocket 长连接模式,cc-connect 主动连接企业微信服务器,无需公网 URL、无需消息加解密、无需 IP 白名单,配置最简单。
### 前置要求
- 企业微信管理员权限
- 一台可运行 cc-connect 的服务器(**无需公网 IP**)
- Claude Code 已安装并配置完成
### 第一步:创建智能机器人
1. 登录 [企业微信管理后台](https://work.weixin.qq.com/wework_admin/frame)
2. 进入 **应用管理****智能机器人** → **创建智能机器人**
3. 填写机器人信息(名称、头像等)
4. 创建完成后,记录以下凭证:
```
BotID: xxxxxxxxxxxxxxxx
Secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
> ⚠️ Secret 只会显示一次,请立即保存!
### 第二步:配置 cc-connect
将凭证配置到 `config.toml` 中:
```toml
[[projects]]
name = "my-project"
[projects.agent]
type = "claudecode"
[projects.agent.options]
work_dir = "/path/to/your/project"
mode = "default"
[[projects.platforms]]
type = "wecom"
[projects.platforms.options]
mode = "websocket"
bot_id = "your-bot-id"
bot_secret = "your-bot-secret"
allow_from = "*"
```
#### 配置项说明
| 配置项 | 必填 | 说明 |
|--------|------|------|
| `mode` | ✅ | 必须为 `"websocket"` |
| `bot_id` | ✅ | 智能机器人 BotID |
| `bot_secret` | ✅ | 智能机器人 Secret |
| `allow_from` | ❌ | 允许的用户 ID(默认 `"*"` 允许所有) |
### 第三步:启动并验证
```bash
cc-connect
```
你应该看到类似日志:
```
level=INFO msg="wecom-ws: connecting" endpoint=wss://openws.work.weixin.qq.com
level=INFO msg="wecom-ws: subscribed successfully" bot_id=your-bot-id
```
在企业微信中找到你的机器人,发送一条消息测试即可。
### 技术细节
- **连接地址**`wss://openws.work.weixin.qq.com`
- **认证方式**:连接后发送 `aibot_subscribe`bot_id + secret
- **心跳**:每 30 秒发送 `ping`
- **自动重连**:连接断开后指数退避重连(1s → 2s → 4s → ... → 30s max
- **图片回传**:通过 `aibot_upload_media_*` 上传临时素材,再用 `aibot_send_msg` 发送图片
- **限制**:同一机器人仅支持 1 个长连接;30 条/分钟、1000 条/小时
---
## 模式二:Webhook 回调
> 💡 推荐优先使用上方的 WebSocket 长连接模式。只有在你已有企业微信自建应用回调部署、或需要兼容旧配置时,再使用 Webhook 回调模式。
### 前置要求
- 企业微信管理员权限
- 一台可运行 cc-connect 的服务器
- **公网可访问的 URL**(用于接收企业微信回调)
- Claude Code 已安装并配置完成
---
## 第一步:创建企业微信自建应用
### 1.1 进入管理后台
登录 [企业微信管理后台](https://work.weixin.qq.com/wework_admin/frame)。
### 1.2 创建应用
1. 进入 **应用管理****自建** → **创建应用**
2. 填写应用信息:
| 字段 | 填写建议 |
|------|---------|
| 应用名称 | `cc-connect` |
| 应用Logo | 上传一个喜欢的图标 |
| 可见范围 | 选择需要使用的部门/成员 |
### 1.3 记录凭证
创建完成后,记录以下信息:
```
AgentId: 1000002
Secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
> ⚠️ Secret 只会显示一次,请立即保存!
---
## 第二步:获取企业 ID
1. 在管理后台首页,点击 **我的企业**
2. 在页面底部找到 **企业ID (CorpId)**
3. 复制保存
```
CorpId: wwxxxxxxxxxxxxxx
```
---
## 第三步:配置接收消息
### 3.1 进入消息配置
进入你创建的应用 → **接收消息** → **设置API接收**
### 3.2 填写配置
| 字段 | 填写内容 |
|------|---------|
| **URL** | `https://你的公网域名/wecom/callback`(见第四步) |
| **Token** | 自定义一个随机字符串 |
| **EncodingAESKey** | 点击「随机获取」生成(43 个字符) |
> ⚠️ **暂时不要点保存!** 需要先启动 cc-connect 再回来保存(因为保存时企业微信会立即验证回调 URL)。
### 3.3 记录配置
把 Token 和 EncodingAESKey 记下来,后面配置 config.toml 要用。
---
## 第四步:配置公网访问
企业微信需要能够访问你的回调 URL。推荐方案:
### 方案 Acloudflared tunnel(推荐,免费)
```bash
# 安装
# macOS: brew install cloudflared
# Linux: 参考 https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/
# 快速启动(会生成一个临时公网 URL)
cloudflared tunnel --url http://localhost:8081
```
启动后会输出类似 `https://xxx-xxx.trycloudflare.com`,将其作为回调 URL 的域名。
### 方案 B:ngrok(开发测试用)
```bash
ngrok http 8081
```
### 方案 C:有公网 IP 的服务器 + Nginx
```nginx
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location /wecom/callback {
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
---
## 第五步:配置企业可信 IP
企业微信要求调用 API 的服务器 IP 在白名单中。
### 5.1 查询服务器出口 IP
```bash
curl -s https://ifconfig.me
```
> 如果你的出口 IP 是动态的(如家用宽带),可以使用 VPS 正向代理方案,见后文「动态 IP 场景」。
### 5.2 添加到白名单
1. 进入 **应用管理** → 选择你的应用
2. 滚动到底部,找到 **企业可信IP**
3. 点击 **配置**,添加你的出口 IP
---
## 第六步:配置 cc-connect
将凭证配置到 `config.toml` 中:
```toml
[[projects]]
name = "my-project"
[projects.agent]
type = "claudecode"
[projects.agent.options]
work_dir = "/path/to/your/project"
mode = "default"
[[projects.platforms]]
type = "wecom"
[projects.platforms.options]
corp_id = "wwxxxxxxxxxxxxxx"
corp_secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
agent_id = "1000002"
callback_token = "你在第三步设置的Token"
callback_aes_key = "你在第三步获取的EncodingAESKey"
port = "8081"
callback_path = "/wecom/callback"
api_base_url = "https://qyapi.weixin.qq.com"
enable_markdown = false
```
### 配置项说明
| 配置项 | 必填 | 说明 |
|--------|------|------|
| `corp_id` | ✅ | 企业 ID |
| `corp_secret` | ✅ | 应用 Secret |
| `agent_id` | ✅ | 应用 AgentId |
| `callback_token` | ✅ | 回调 Token |
| `callback_aes_key` | ✅ | 回调 EncodingAESKey43字符) |
| `port` | ❌ | Webhook 监听端口(默认 `8081` |
| `callback_path` | ❌ | Webhook 路径(默认 `/wecom/callback` |
| `api_base_url` | ❌ | 企业微信 API 基础地址(默认 `https://qyapi.weixin.qq.com` |
| `enable_markdown` | ❌ | 是否发送 Markdown 消息(默认 `false` |
| `proxy` | ❌ | HTTP 正向代理地址(动态 IP 场景使用) |
### 关于 enable_markdown
- `false`(默认):发送纯文本消息,**企业微信应用和个人微信都能正常显示**
- `true`:发送 Markdown 格式消息,**仅企业微信应用内可渲染**,个人微信会显示「暂不支持的消息类型」
> 💡 如果你的用户主要通过个人微信使用,建议保持 `false`
---
## 第七步:启动并验证
### 7.1 启动 cc-connect
```bash
cc-connect
# 或指定配置文件
cc-connect -config /path/to/config.toml
```
你应该看到类似日志:
```
level=INFO msg="platform started" project=my-project platform=wecom
level=INFO msg="cc-connect is running" projects=1
level=INFO msg="wecom: webhook server listening" port=8081 path=/wecom/callback
```
### 7.2 确保公网隧道在运行
```bash
# 确认 cloudflared / ngrok 正在运行并转发到 8081 端口
cloudflared tunnel --url http://localhost:8081
```
### 7.3 回到企业微信保存回调配置
1. 回到企业微信管理后台 → 你的应用 → 接收消息
2. 确认 URL 填写正确(cloudflared 生成的公网 URL + `/wecom/callback`
3. 点击 **保存**
4. 如果验证通过,配置完成!
---
## 第八步:关联个人微信(可选)
如果希望**个人微信**也能直接与 AI 对话:
1. 登录企业微信管理后台
2. 进入 **我的企业** → **微信插件**
3. 用个人微信扫描页面上的二维码
4. 关联后,个人微信中会出现企业微信的应用入口
> 💡 关联后,个人微信用户可以直接发送消息给应用,无需安装企业微信。
---
## 动态 IP 场景
如果你的服务器没有固定公网 IP(如家用宽带),企业微信可信 IP 白名单无法使用动态 IP。解决方案:
### 使用 VPS 正向代理
1. 在一台有固定公网 IP 的 VPS 上安装 tinyproxy
```bash
# Ubuntu/Debian
apt install tinyproxy
# 编辑配置:允许你的机器访问
vim /etc/tinyproxy/tinyproxy.conf
# 添加: Allow your-home-ip
systemctl restart tinyproxy
```
2. 在 cc-connect 配置中添加 proxy
```toml
[projects.platforms.options]
# ... 其他配置 ...
proxy = "http://vps-ip:8888"
```
3. 将 VPS 的公网 IP 添加到企业可信 IP 白名单
这样 cc-connect 调用企业微信 API 时会通过 VPS 代理,出口 IP 固定为 VPS 的 IP。
---
## 架构图
```
┌─────────────────────────────────────────────────────────────┐
│ 企业微信 / 个人微信 │
│ 服务器 │
│ │ │
│ 加密 XML 回调 │
└────────────────────────┼─────────────────────────────────────┘
│ HTTPS (需要公网 URL)
┌─────────────────────────────────────────────────────────────┐
│ 你的服务器 │
│ │
│ cloudflared ──→ cc-connect ──→ Claude Code CLI │
│ / ngrok │ │
│ │ (可选) proxy │
│ ▼ │
│ 企业微信 API ──→ VPS 正向代理 │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 常见问题
### Q: 回调验证失败?
1. 确认 cc-connect 已启动且 webhook server 在监听
2. 确认公网隧道(cloudflared/ngrok)正在运行
3. 检查 URL 是否能公网访问:`curl https://你的域名/wecom/callback`
4. 检查 Token 和 EncodingAESKey 是否与管理后台一致
### Q: 消息发不出去?
1. 检查日志是否有 `get access_token failed` 错误
2. 确认出口 IP 在企业可信 IP 白名单中
3. 如果使用代理,确认代理服务正常运行
### Q: 报错 `60020` (not allow to access from your ip)
日志中会提示实际的出口 IP,将该 IP 添加到企业可信 IP 白名单。
### Q: 个人微信显示「暂不支持的消息类型」?
`enable_markdown` 设为 `false`(默认值),改为发送纯文本消息。
### Q: 动态 IP 导致发送失败?
参考上文「动态 IP 场景」,使用 VPS 正向代理。
---
## 参考链接
- [企业微信管理后台](https://work.weixin.qq.com/wework_admin/frame)
- [企业微信开发文档](https://developer.work.weixin.qq.com/document/)
- [消息加解密说明](https://developer.work.weixin.qq.com/document/path/90307)
- [Cloudflare Tunnel 文档](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/)
---
## 下一步
- [接入飞书](./feishu.md)
- [接入钉钉](./dingtalk.md)
- [接入微博](./weibo.md)
- [接入 Telegram](./telegram.md)
- [接入 Slack](./slack.md)
- [接入 Discord](./discord.md)
- [返回首页](../README.md)
+210
View File
@@ -0,0 +1,210 @@
# 微博私信接入指南
本文档介绍如何将 **cc-connect** 接入微博私信,让你可以通过微博私信远程调用 AI 编程 Agent。
## 前置要求
- 微博账号
- 一台可运行 cc-connect 的设备(无需公网 IP
- AI 编程 AgentClaude Code、Codex 等)已安装并配置完成
> 💡 **优势**:使用 WebSocket 长连接,无需公网 IP、无需域名、无需反向代理
---
## 第一步:注册微博开放平台应用
### 1.1 进入微博开放平台
访问 [微博开放平台](https://open.weibo.com/),通过微博龙虾助手注册应用。
### 1.2 创建应用
按照平台指引创建一个新的应用,获取 Open IM 的 `app_id``app_secret`
> ⚠️ **重要**:请妥善保存这两个凭证,后续配置 cc-connect 时需要用到。
---
## 第二步:配置 cc-connect
### 2.1 编辑配置文件
将凭证配置到 cc-connect 的 `config.toml` 中:
```toml
[[projects]]
name = "my-project"
[projects.agent]
type = "claudecode"
[projects.agent.options]
work_dir = "/path/to/your/project"
[[projects.platforms]]
type = "weibo"
[projects.platforms.options]
app_id = "your-weibo-app-id"
app_secret = "your-weibo-app-secret"
```
### 2.2 使用 CLI 引导配置(推荐)
也可以使用交互式 CLI 来配置:
```bash
cc-connect new
# 选择 weibo 平台,按提示输入 app_id 和 app_secret
```
### 2.3 可选配置项
```toml
[projects.platforms.options]
app_id = "your-weibo-app-id"
app_secret = "your-weibo-app-secret"
# allow_from = "*" # 允许的微博用户 ID,逗号分隔;"*" 表示所有(默认)
# token_endpoint = "" # 自定义 token 接口地址(默认:https://open-im.api.weibo.com/open/auth/ws_token
# ws_endpoint = "" # 自定义 WebSocket 地址(默认:ws://open-im.api.weibo.com/ws/stream
```
---
## 第三步:启动 cc-connect
### 3.1 启动服务
```bash
cc-connect
# 或指定配置文件
cc-connect -config /path/to/config.toml
```
### 3.2 验证连接
启动后,cc-connect 会自动与微博建立 WebSocket 长连接。你会在日志中看到:
```
level=INFO msg="weibo: authenticated" uid=1234567890
level=INFO msg="weibo: websocket connected"
level=INFO msg="platform started" project=my-project platform=weibo
level=INFO msg="cc-connect is running" projects=1
```
---
## 第四步:开始使用
### 4.1 发送私信
在微博中给你的应用账号发送私信,即可与 AI Agent 对话:
```
用户: 帮我分析一下当前项目的结构
cc-connect: 🤔 思考中...
cc-connect: 🔧 执行: Bash(ls -la)
cc-connect: ✅ 这是一个 Go 项目,包含以下模块...
```
### 4.2 使用命令
所有 cc-connect 命令均可在微博私信中使用:
| 命令 | 功能 |
|------|------|
| `/status` | 查看 Agent 状态 |
| `/new` | 新建会话 |
| `/list` | 查看会话列表 |
| `/stop` | 停止当前会话 |
| `/help` | 查看帮助 |
---
## 连接方式说明
微博私信平台使用 WebSocket 长连接:
```
┌─────────────────────────────────────────────────────────────┐
│ 微博 Open IM │
│ │
│ 用户私信 ──→ open-im.api.weibo.com ──→ WebSocket Stream │
│ │ │
└──────────────────────────────────────────────┼───────────────┘
│ WebSocket 长连接
│ (无需公网IP)
┌─────────────────────────────────────────────────────────────┐
│ 你的本地环境 │
│ │
│ cc-connect ◄──► AI Agent CLI ◄──► 你的项目代码 │
│ │
└─────────────────────────────────────────────────────────────┘
```
| 特性 | 说明 |
|------|------|
| ✅ 无需公网 IP | 内网环境也能接入 |
| ✅ 无需域名 | 不需要配置域名 |
| ✅ 自动重连 | 断线后自动重连(指数退避) |
| ✅ 心跳保活 | 30 秒心跳间隔,40 秒超时检测 |
| ✅ Token 自动刷新 | 过期前自动续期 |
---
## 技术细节
### 消息长度限制
微博私信文本限制约 2000 字符。cc-connect 会自动将超长消息分块发送,接收端会按顺序收到完整内容。
### Token 管理
- 首次启动时通过 `app_id` + `app_secret` 获取 WebSocket Token
- Token 过期前 60 秒自动刷新
- WebSocket 断开时(code 4002 / invalid token)自动清除并重新获取
### 安全建议
- 使用 `allow_from` 限制允许使用的微博用户 ID
- 发送 `/whoami` 获取你的用户 ID
- 不要将 `app_secret` 提交到代码仓库
---
## 常见问题
### Q: 连接后收不到消息?
检查以下项目:
1. cc-connect 服务是否正常运行
2. WebSocket 连接是否建立成功(查看日志)
3. `app_id``app_secret` 是否正确
### Q: 长连接断开怎么办?
cc-connect 内置了自动重连机制(指数退避,最大 10 秒间隔),断开后会自动尝试重新连接。
### Q: 提示 Token 无效?
- Token 过期后会自动刷新,一般无需手动干预
- 如果持续失败,检查 `app_secret` 是否有效
### Q: 消息发送后显示不完整?
微博私信有约 2000 字符的限制,cc-connect 会自动分块发送。如果仍有问题,检查网络连接。
---
## 下一步
- [接入飞书](./feishu.md)
- [接入钉钉](./dingtalk.md)
- [接入 Telegram](./telegram.md)
- [接入 Discord](./discord.md)
- [接入 Slack](./slack.md)
- [返回首页](../README.md)
+143
View File
@@ -0,0 +1,143 @@
# 微信个人号(Weixin / ilink)接入指南
本文档说明如何通过 **cc-connect** 接入**微信个人号**侧的对话能力。底层使用腾讯 **ilink 机器人 HTTP 网关**(与 OpenClaw 插件 `openclaw-weixin` 同类接口:`getUpdates` 长轮询 + `sendMessage` 下发)。
> **说明**:这是「个人微信 + ilink」通道,与 **[企业微信 WeChat Work](wecom.md)**`type = "wecom"`)不是同一套协议,请勿混淆。
---
## 前置要求
- 可运行 cc-connect 的环境(无需公网 IP;ilink 由云端提供)
- 已安装并可正常使用的 Agent(如 Claude Code、Codex 等)
- 使用 **微信(手机端)** 扫码完成 ilink 登录(或由运营商提供 Bearer Token
---
## 推荐流程:一条命令扫码
装好 `cc-connect` 后,在项目目录执行(将 `my-project` 换成你的 `config.toml` 里的项目名,或留空在仅有一个项目时自动选择):
```bash
cc-connect weixin setup --project my-project
```
终端会打印:
1. **二维码**(终端 ASCII)以及 **可复制的 URL**(手机微信打开或扫码均可,取决于网关返回的链接形式)
2. 按提示在手机上 **确认登录**
3. 成功后,命令会把 **`token`Bearer**、**`base_url`**(若网关返回)、**`account_id`ilink_bot_id** 等写回 `config.toml`
4. 若当前 `allow_from` 为空且你使用了 `--set-allow-from-empty`(默认开启),会尝试填入扫码关联的 **微信用户 ID**,便于限制谁可以使用机器人
### 命令对照
| 命令 | 作用 | 何时使用 |
|------|------|----------|
| `weixin setup` | 无 `--token` → 走扫码;有 `--token` → 等同绑定 | **默认首选** |
| `weixin new` | 强制扫码,不接受 `--token` | 明确只要重新扫码 |
| `weixin bind` | 强制只写 token,必须 `--token` | 已有 Token(例如从 OpenClaw 导出) |
已有 Token 时:
```bash
cc-connect weixin bind --project my-project --token '<你的_Bearer_Token>'
# 或
cc-connect weixin setup --project my-project --token '<你的_Bearer_Token>'
```
若校验失败,可检查 `--api-url` 是否与运营商一致(默认 `https://ilinkai.weixin.qq.com`),或使用 `--skip-verify` 仅写入配置(不推荐生产环境)。
### 常用参数
| 参数 | 说明 |
|------|------|
| `--config` | 指定 `config.toml` 路径 |
| `--project` | 目标项目名;不存在会自动创建并挂上 `weixin` 平台 |
| `--platform-index` | 同一项目多个 `weixin` 平台时,按 1 基索引选择 |
| `--api-url` | ilink 网关根地址(无尾部路径) |
| `--cdn-url` | 可选,同时写入 `cdn_base_url` |
| `--timeout` | 等待扫码秒数,默认 `480` |
| `--qr-image` | 将二维码 URL 导出为 PNG 路径 |
| `--route-tag` | 若运营商要求,设置 `SKRouteTag` 请求头 |
| `--bot-type` | `get_bot_qrcode``bot_type`,默认 `3` |
| `--debug` | 打印 HTTP 调试信息 |
---
## 配置说明(config.toml
典型片段如下(具体键名以 `config.example.toml` 为准):
```toml
[[projects.platforms]]
type = "weixin"
[projects.platforms.options]
token = "ilink_bot_bearer_token" # 必填;扫码或 bind 写入
# base_url = "https://ilinkai.weixin.qq.com" # 可选,默认同左
# cdn_base_url = "https://novac2c.cdn.weixin.qq.com/c2c" # 可选,CDN 根路径
# allow_from = "user@im.wechat" # 建议限制使用者;逗号分隔或 "*"
# account_id = "default" # 多账号时区分状态目录,见下
# route_tag = "" # 与 CLI --route-tag 一致
# long_poll_timeout_ms = 35000
# proxy = "" # 可选 HTTP 代理
```
### `allow_from`
- 空或 `"*"` 表示不限制发送者(**不安全**,仅建议本机调试)。
- 生产环境请填允许的 **ilink 用户 ID**(形如 `xxx@im.wechat`),多个用英文逗号分隔。
- 扫码成功后,若开启默认的「空则回填」,会把扫码用户写入 `allow_from`(仍建议你核对后再上线)。
### `account_id` 与状态目录
多微信账号或多机器人时,可用不同 `account_id` 隔离本地状态。状态文件默认在:
`<data_dir>/weixin/<project>/<account_id>/`
其中含 `get_updates` 游标、`context_token` 缓存等,**勿手动泄露**。
### `context_token`(首次对话)
网关下发消息时可能带 `context_token`cc-connect 会缓存并在回复时使用。
**首次连接**:请先启动 cc-connect,再用允许的微信账号 **给机器人发一条消息**,完成关联后再使用 `/new` 等指令。
---
## 能力与限制(摘要)
- **文字、引用、语音转写文本**:与网关一致。
- **图片 / 文件 / 视频 / 语音文件**:支持从微信 CDN 下载并按 AES-128-ECB 解密后交给 Agent(需正确配置 `cdn_base_url` 等)。
- **出站图片与文件**:平台实现了 `ImageSender` / `FileSender`,可通过 `cc-connect send --image` / `--file` 等能力下发(需引擎侧已支持附件发送)。
- **语音 SILK**:无转写文字时可走 STT(需配置语音转写且通常依赖 ffmpeg)。
---
## 精简编译(可选)
若不需要本通道,构建时可排除:
```bash
go build -tags no_weixin ./cmd/cc-connect
```
详见仓库 `Makefile` / `AGENTS.md` 中的构建标签说明。
---
## 故障排查
| 现象 | 建议 |
|------|------|
| 扫码无反应 / 超时 | 检查网络、 `--api-url``--timeout`;重试 `weixin setup` |
| 写入配置后仍收不到消息 | 确认 `allow_from`、进程已重启、微信端已发消息触发 `context_token` |
| 媒体无法解密 | 核对 `cdn_base_url`、网关返回的加密字段是否齐全 |
| 返回 errcode `-14` 等 | 多为会话过期,按日志提示暂停轮询后重新登录或稍后再试 |
---
## 相关链接
- 仓库内示例配置:[config.example.toml](../config.example.toml)
- 使用指南中的 CLI 摘要:[usage.zh-CN.md](./usage.zh-CN.md)(「微信个人号配置 CLI」)
- OpenClaw 同类插件(参考实现):`openclaw-weixin`
+113
View File
@@ -0,0 +1,113 @@
# WPS Xiezuo Platform Setup Guide
This guide explains how to connect **cc-connect** to WPS Xiezuo (WPS 365 collaboration) so users can talk to an AI coding agent from WPS chats.
## Prerequisites
- A WPS Open Platform application with app chat events enabled
- `app_id` and `app_secret` for the application
- A machine running cc-connect; no public IP is required
- An agent such as Claude Code, Codex, or Gemini CLI configured in cc-connect
## Connection Model
The platform uses WPS event WebSocket delivery and WPS REST APIs:
- Incoming events: WebSocket at `wss://openapi.wps.cn/v7/event/ws`
- Authentication: KSO-1 HMAC-SHA256 headers
- Event payloads: encrypted with AES-256-CBC and verified with HMAC-SHA256 signatures
- Outgoing replies: REST API with a cached `client_credentials` access token
cc-connect sends ACK frames on the WebSocket writer loop, so no public callback URL is needed.
## Configure WPS
In the WPS Open Platform console:
1. Create or select an application.
2. Enable app chat/message capabilities for the application.
3. Enable event WebSocket delivery.
4. Subscribe to message events:
- `kso.app_chat.message`
- `kso.app_chat.message.recall` if recall notifications are needed
5. Grant the permissions needed to send app chat messages and reactions.
6. Copy the application `app_id` and `app_secret`.
The exact console names may vary by WPS tenant and app type. If the connection fails with authorization errors, verify that the app is published/enabled for the target organization and has the required app chat permissions.
## Configure cc-connect
Add `wps-xiezuo` to a project in `config.toml`:
```toml
[[projects]]
name = "my-project"
[projects.agent]
type = "claudecode"
[projects.agent.options]
work_dir = "/path/to/your/project"
[[projects.platforms]]
type = "wps-xiezuo"
[projects.platforms.options]
app_id = "your-wps-xiezuo-app-id"
app_secret = "your-wps-xiezuo-app-secret"
allow_from = "*" # optional; set to WPS user IDs in production
clean_reply = false # optional; strip thinking/tool progress lines
```
### Options
| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `app_id` | yes | - | WPS Open Platform application ID |
| `app_secret` | yes | - | WPS Open Platform application secret |
| `allow_from` | no | all users | Comma-separated WPS user IDs allowed to use the bot; set this in production |
| `clean_reply` | no | `false` | Removes common thinking/tool progress lines from replies before sending |
| `base_url` | no | `https://openapi.wps.cn` | Override WPS REST API base URL for private or test environments |
## Start and Verify
Start cc-connect:
```bash
cc-connect -config /path/to/config.toml
```
Expected logs include:
```text
level=INFO msg="wps-xiezuo: connecting" endpoint=wss://openapi.wps.cn/v7/event/ws
level=INFO msg="wps-xiezuo: connected"
level=INFO msg="platform started" project=my-project platform=wps-xiezuo
```
Send a message to the WPS app chat. cc-connect should receive the encrypted event, ACK it, forward the text to the configured agent, and send the reply back through the WPS message API.
## Security Notes
- Always set `allow_from` for production deployments.
- Keep `app_secret` out of source control. Environment variable substitution is supported in `config.toml`, for example `app_secret = "${WPS_XIEZUO_APP_SECRET}"`.
- Debug logs avoid printing decrypted WPS message payloads by default.
## Troubleshooting
**Connection fails immediately**
- Confirm `app_id` and `app_secret` are correct.
- Confirm the app has WebSocket event delivery enabled.
- Confirm the app is available to the organization where you are testing.
**Messages arrive but replies fail**
- Confirm the app has message send permissions.
- Check whether the tenant requires the `/oauth2/token` or `/openapi/oauth2/token` token endpoint; cc-connect tries both.
- Verify the target chat allows app messages.
**Bot responds to unexpected users**
- Set `allow_from` to a comma-separated list of WPS user IDs.
- Users can send `/whoami` to discover the ID used by cc-connect.