openclaw-channel-xiaozhu 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/dist/channel.d.ts +96 -0
- package/dist/channel.js +225 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +15 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.js +609 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.js +4 -0
- package/dist/utils/coalescer.d.ts +18 -0
- package/dist/utils/coalescer.js +35 -0
- package/dist/utils/rate-limiter.d.ts +16 -0
- package/dist/utils/rate-limiter.js +30 -0
- package/dist/utils/ttl-set.d.ts +13 -0
- package/dist/utils/ttl-set.js +39 -0
- package/docs/API.md +471 -0
- package/openclaw.plugin.json +13 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OpenClaw Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# openclaw-channel-xiaozhu
|
|
2
|
+
|
|
3
|
+
小助 — OpenClaw 通用渠道插件,通过标准 HTTP/SSE 接口将任意客户端接入 OpenClaw 对话引擎。与钉钉(`openclaw-channel-dingtalk`)、飞书(`openclaw-channel-feishu`)并列,是面向自定义客户端的通用接入渠道。
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 同步聊天 (`POST /api/chat`) 与 SSE 流式聊天 (`POST /api/chat/stream`),流式响应支持文本合并
|
|
8
|
+
- 滑动窗口限流 + 突发控制
|
|
9
|
+
- 消息去重(TTL 集合)
|
|
10
|
+
- 发送者白名单 (`allowFrom`)
|
|
11
|
+
- 群聊支持 (`chatType: "group"`)
|
|
12
|
+
- 多媒体消息:图片、音频、文件(base64 / URL)
|
|
13
|
+
- 媒体文件托管 (`GET /api/media/:id`)
|
|
14
|
+
- 语音转文字 (`POST /api/stt`,基于 Whisper)
|
|
15
|
+
- 文字转语音 (`POST /api/tts`,基于 edge-tts)
|
|
16
|
+
- Webhook 外发消息 (`POST /api/webhook/register`)
|
|
17
|
+
- 可配置 CORS 来源
|
|
18
|
+
- 常量时间 Token 比较(防时序攻击)
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### Prerequisites
|
|
23
|
+
|
|
24
|
+
- Node.js >= 20
|
|
25
|
+
- OpenClaw >= 2026.1
|
|
26
|
+
|
|
27
|
+
### Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install openclaw-channel-xiaozhu
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Build
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm run build
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Deploy
|
|
40
|
+
|
|
41
|
+
将构建产物部署到 OpenClaw 插件目录,或在 `openclaw.json` 中注册本渠道即可。
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
在 `openclaw.json` 的 `channels` 中添加:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"channels": [
|
|
50
|
+
{
|
|
51
|
+
"type": "channel-xiaozhu",
|
|
52
|
+
"config": {
|
|
53
|
+
"port": 3000,
|
|
54
|
+
"token": "your-secret-token",
|
|
55
|
+
"rateLimitWindowMs": 60000,
|
|
56
|
+
"rateLimitMax": 60,
|
|
57
|
+
"rateLimitBurst": 10,
|
|
58
|
+
"dedupTtlMs": 5000,
|
|
59
|
+
"allowFrom": ["*"],
|
|
60
|
+
"webhookUrl": "",
|
|
61
|
+
"allowOrigins": ["*"]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
69
|
+
|------|------|--------|------|
|
|
70
|
+
| `port` | number | `3000` | HTTP 监听端口 |
|
|
71
|
+
| `token` | string | — | 鉴权 Token(必填) |
|
|
72
|
+
| `rateLimitWindowMs` | number | `60000` | 限流滑动窗口(毫秒) |
|
|
73
|
+
| `rateLimitMax` | number | `60` | 窗口内最大请求数 |
|
|
74
|
+
| `rateLimitBurst` | number | `10` | 突发请求上限 |
|
|
75
|
+
| `dedupTtlMs` | number | `5000` | 消息去重 TTL(毫秒) |
|
|
76
|
+
| `allowFrom` | string[] | `["*"]` | 允许的发送者 ID,`*` 表示全部 |
|
|
77
|
+
| `webhookUrl` | string | `""` | Webhook 回调地址,为空则不启用 |
|
|
78
|
+
| `allowOrigins` | string[] | `["*"]` | CORS 允许的来源 |
|
|
79
|
+
|
|
80
|
+
## API Overview
|
|
81
|
+
|
|
82
|
+
| Method | Path | Auth | Description |
|
|
83
|
+
|--------|------|------|-------------|
|
|
84
|
+
| `POST` | `/api/chat` | Yes | 同步聊天 |
|
|
85
|
+
| `POST` | `/api/chat/stream` | Yes | SSE 流式聊天 |
|
|
86
|
+
| `GET` | `/api/media/:id` | Yes | 获取媒体文件 |
|
|
87
|
+
| `POST` | `/api/stt` | Yes | 语音转文字 |
|
|
88
|
+
| `POST` | `/api/tts` | Yes | 文字转语音 |
|
|
89
|
+
| `POST` | `/api/webhook/register` | Yes | 注册 Webhook |
|
|
90
|
+
| `GET` | `/api/health` | No | 健康检查 |
|
|
91
|
+
|
|
92
|
+
完整 API 文档见 [docs/API.md](docs/API.md)。
|
|
93
|
+
|
|
94
|
+
## Environment Variables
|
|
95
|
+
|
|
96
|
+
| 变量 | 说明 |
|
|
97
|
+
|------|------|
|
|
98
|
+
| `WHISPER_BIN` | Whisper 可执行文件路径,留空则从 PATH 查找 |
|
|
99
|
+
| `EDGE_TTS_BIN` | edge-tts 可执行文件路径,留空则从 PATH 查找 |
|
|
100
|
+
|
|
101
|
+
可复制 `.env.example` 为 `.env` 进行配置。
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git clone https://github.com/openclaw/openclaw-channel-xiaozhu.git
|
|
107
|
+
cd openclaw-channel-xiaozhu
|
|
108
|
+
npm install
|
|
109
|
+
npm run build
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Project Structure
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
openclaw-channel-xiaozhu/
|
|
116
|
+
├── src/
|
|
117
|
+
│ ├── index.ts # 插件入口
|
|
118
|
+
│ ├── channel.ts # Channel 插件对象(四个适配器)
|
|
119
|
+
│ ├── server.ts # HTTP 服务器(全功能)
|
|
120
|
+
│ ├── types.ts # 类型定义
|
|
121
|
+
│ └── utils/
|
|
122
|
+
│ ├── rate-limiter.ts # 滑动窗口速率限制器
|
|
123
|
+
│ ├── ttl-set.ts # TTL 集合(消息去重)
|
|
124
|
+
│ └── coalescer.ts # SSE 文本合并缓冲
|
|
125
|
+
├── openclaw.plugin.json # 插件声明
|
|
126
|
+
├── tsconfig.json
|
|
127
|
+
└── package.json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Architecture
|
|
131
|
+
|
|
132
|
+
本插件实现 OpenClaw 渠道接口的四个适配器:
|
|
133
|
+
|
|
134
|
+
1. **Config** — 读取并校验渠道配置,合并默认值
|
|
135
|
+
2. **Gateway** — 启动 HTTP 服务,处理入站消息,转发至 OpenClaw 对话引擎
|
|
136
|
+
3. **Outbound** — 将引擎回复通过 Webhook 推送到外部系统
|
|
137
|
+
4. **Status** — 暴露 `/api/health`,报告运行时间、请求统计、限流状态
|
|
138
|
+
|
|
139
|
+
消息流:`客户端 → Gateway → OpenClaw Engine → Outbound → Webhook`
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel 插件对象 — config / gateway / outbound / status 四个适配器
|
|
3
|
+
*/
|
|
4
|
+
import type { HttpApiAccount, PluginRuntime, ChannelConfig, GatewayContext } from "./types.js";
|
|
5
|
+
export declare function setRuntime(rt: PluginRuntime): void;
|
|
6
|
+
export declare const httpChannelPlugin: {
|
|
7
|
+
id: string;
|
|
8
|
+
meta: {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
selectionLabel: string;
|
|
12
|
+
blurb: string;
|
|
13
|
+
order: number;
|
|
14
|
+
aliases: string[];
|
|
15
|
+
};
|
|
16
|
+
capabilities: {
|
|
17
|
+
chatTypes: readonly ["direct", "group"];
|
|
18
|
+
reactions: boolean;
|
|
19
|
+
threads: boolean;
|
|
20
|
+
media: boolean;
|
|
21
|
+
nativeCommands: boolean;
|
|
22
|
+
blockStreaming: boolean;
|
|
23
|
+
};
|
|
24
|
+
config: {
|
|
25
|
+
listAccountIds: (cfg: ChannelConfig) => string[];
|
|
26
|
+
resolveAccount: (cfg: ChannelConfig, accountId: string) => HttpApiAccount;
|
|
27
|
+
defaultAccountId: (_cfg: ChannelConfig) => string;
|
|
28
|
+
isConfigured: (account: HttpApiAccount) => boolean;
|
|
29
|
+
describeAccount: (account: HttpApiAccount) => {
|
|
30
|
+
accountId: string;
|
|
31
|
+
name: string;
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
configured: boolean;
|
|
34
|
+
};
|
|
35
|
+
setAccountEnabled: ({ cfg, accountId, enabled }: {
|
|
36
|
+
cfg: ChannelConfig;
|
|
37
|
+
accountId: string;
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
}) => {
|
|
40
|
+
channels: {
|
|
41
|
+
"channel-xiaozhu": {
|
|
42
|
+
enabled: boolean;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
deleteAccount: ({ cfg, accountId }: {
|
|
47
|
+
cfg: ChannelConfig;
|
|
48
|
+
accountId: string;
|
|
49
|
+
}) => {
|
|
50
|
+
channels: {
|
|
51
|
+
[x: string]: Record<string, unknown>;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
gateway: {
|
|
56
|
+
startAccount: (ctx: GatewayContext) => Promise<void>;
|
|
57
|
+
};
|
|
58
|
+
outbound: {
|
|
59
|
+
deliveryMode: "direct";
|
|
60
|
+
textChunkLimit: number;
|
|
61
|
+
sendText: (opts: Record<string, unknown>) => Promise<{
|
|
62
|
+
channel: string;
|
|
63
|
+
ok: boolean;
|
|
64
|
+
error: Error;
|
|
65
|
+
} | {
|
|
66
|
+
channel: string;
|
|
67
|
+
ok: boolean;
|
|
68
|
+
error?: undefined;
|
|
69
|
+
}>;
|
|
70
|
+
sendMedia: (opts: Record<string, unknown>) => Promise<{
|
|
71
|
+
channel: string;
|
|
72
|
+
ok: boolean;
|
|
73
|
+
error: Error;
|
|
74
|
+
} | {
|
|
75
|
+
channel: string;
|
|
76
|
+
ok: boolean;
|
|
77
|
+
error?: undefined;
|
|
78
|
+
}>;
|
|
79
|
+
};
|
|
80
|
+
status: {
|
|
81
|
+
defaultRuntime: {
|
|
82
|
+
accountId: string;
|
|
83
|
+
running: boolean;
|
|
84
|
+
connected: boolean;
|
|
85
|
+
lastStartAt: null;
|
|
86
|
+
lastError: null;
|
|
87
|
+
};
|
|
88
|
+
probeAccount: ({ account }: {
|
|
89
|
+
account: HttpApiAccount;
|
|
90
|
+
timeoutMs?: number;
|
|
91
|
+
}) => Promise<{
|
|
92
|
+
ok: boolean;
|
|
93
|
+
latencyMs: number;
|
|
94
|
+
}>;
|
|
95
|
+
};
|
|
96
|
+
};
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel 插件对象 — config / gateway / outbound / status 四个适配器
|
|
3
|
+
*/
|
|
4
|
+
import { startHttpServer, getRuntimeWebhookUrl } from "./server.js";
|
|
5
|
+
const CHANNEL_ID = "channel-xiaozhu";
|
|
6
|
+
const DEFAULT_PORT = 18800;
|
|
7
|
+
let pluginRuntime = null;
|
|
8
|
+
export function setRuntime(rt) {
|
|
9
|
+
pluginRuntime = rt;
|
|
10
|
+
}
|
|
11
|
+
function getChannelConfig(cfg) {
|
|
12
|
+
return cfg?.channels?.[CHANNEL_ID] || {};
|
|
13
|
+
}
|
|
14
|
+
export const httpChannelPlugin = {
|
|
15
|
+
id: CHANNEL_ID,
|
|
16
|
+
meta: {
|
|
17
|
+
id: CHANNEL_ID,
|
|
18
|
+
label: "小助",
|
|
19
|
+
selectionLabel: "小助",
|
|
20
|
+
blurb: "通过 HTTP/SSE 接口接入对话的通用渠道",
|
|
21
|
+
order: 90,
|
|
22
|
+
aliases: ["xiaozhu", "http", "api", "rest"],
|
|
23
|
+
},
|
|
24
|
+
capabilities: {
|
|
25
|
+
chatTypes: ["direct", "group"],
|
|
26
|
+
reactions: false,
|
|
27
|
+
threads: false,
|
|
28
|
+
media: true,
|
|
29
|
+
nativeCommands: false,
|
|
30
|
+
blockStreaming: true,
|
|
31
|
+
},
|
|
32
|
+
// ── config 适配器 ──
|
|
33
|
+
config: {
|
|
34
|
+
listAccountIds: (cfg) => {
|
|
35
|
+
const ch = getChannelConfig(cfg);
|
|
36
|
+
if (ch && typeof ch === "object" && !Array.isArray(ch)) {
|
|
37
|
+
return ["default"];
|
|
38
|
+
}
|
|
39
|
+
return [];
|
|
40
|
+
},
|
|
41
|
+
resolveAccount: (cfg, accountId) => {
|
|
42
|
+
const ch = getChannelConfig(cfg);
|
|
43
|
+
return {
|
|
44
|
+
accountId,
|
|
45
|
+
enabled: ch.enabled !== false,
|
|
46
|
+
port: ch.port || DEFAULT_PORT,
|
|
47
|
+
token: ch.token || "",
|
|
48
|
+
rateLimitWindowMs: ch.rateLimitWindowMs,
|
|
49
|
+
rateLimitMax: ch.rateLimitMax,
|
|
50
|
+
rateLimitBurst: ch.rateLimitBurst,
|
|
51
|
+
dedupTtlMs: ch.dedupTtlMs,
|
|
52
|
+
allowFrom: ch.allowFrom,
|
|
53
|
+
webhookUrl: ch.webhookUrl,
|
|
54
|
+
allowOrigins: ch.allowOrigins,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
defaultAccountId: (_cfg) => "default",
|
|
58
|
+
isConfigured: (account) => {
|
|
59
|
+
return !!account.token;
|
|
60
|
+
},
|
|
61
|
+
describeAccount: (account) => ({
|
|
62
|
+
accountId: account.accountId,
|
|
63
|
+
name: `HTTP API :${account.port}`,
|
|
64
|
+
enabled: account.enabled,
|
|
65
|
+
configured: !!account.token,
|
|
66
|
+
}),
|
|
67
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
|
68
|
+
const ch = getChannelConfig(cfg) || {};
|
|
69
|
+
return { ...cfg, channels: { ...cfg.channels, [CHANNEL_ID]: { ...ch, enabled } } };
|
|
70
|
+
},
|
|
71
|
+
deleteAccount: ({ cfg, accountId }) => {
|
|
72
|
+
const channels = { ...cfg.channels };
|
|
73
|
+
delete channels[CHANNEL_ID];
|
|
74
|
+
return { ...cfg, channels };
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
// ── gateway 适配器 ──
|
|
78
|
+
gateway: {
|
|
79
|
+
startAccount: async (ctx) => {
|
|
80
|
+
const account = ctx.account;
|
|
81
|
+
const { abortSignal, log } = ctx;
|
|
82
|
+
ctx.setStatus({
|
|
83
|
+
accountId: account.accountId,
|
|
84
|
+
running: true,
|
|
85
|
+
connected: false,
|
|
86
|
+
lastStartAt: Date.now(),
|
|
87
|
+
});
|
|
88
|
+
const rt = pluginRuntime || ctx.runtime;
|
|
89
|
+
const cfg = ctx.cfg;
|
|
90
|
+
try {
|
|
91
|
+
await startHttpServer({
|
|
92
|
+
port: account.port,
|
|
93
|
+
token: account.token,
|
|
94
|
+
accountId: account.accountId,
|
|
95
|
+
account,
|
|
96
|
+
runtime: rt,
|
|
97
|
+
config: cfg,
|
|
98
|
+
log,
|
|
99
|
+
abortSignal,
|
|
100
|
+
onReady: () => {
|
|
101
|
+
ctx.setStatus({ accountId: account.accountId, connected: true });
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
107
|
+
log.error(`[${CHANNEL_ID}] Failed to start HTTP server:`, msg);
|
|
108
|
+
ctx.setStatus({
|
|
109
|
+
accountId: account.accountId,
|
|
110
|
+
running: false,
|
|
111
|
+
connected: false,
|
|
112
|
+
lastError: msg,
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// 等待关闭信号
|
|
117
|
+
await new Promise((resolve) => {
|
|
118
|
+
abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
|
119
|
+
});
|
|
120
|
+
ctx.setStatus({
|
|
121
|
+
accountId: account.accountId,
|
|
122
|
+
running: false,
|
|
123
|
+
connected: false,
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
// ── outbound 适配器 ──
|
|
128
|
+
outbound: {
|
|
129
|
+
deliveryMode: "direct",
|
|
130
|
+
textChunkLimit: 4000,
|
|
131
|
+
sendText: async (opts) => {
|
|
132
|
+
const account = opts?.account;
|
|
133
|
+
const text = opts?.text || "";
|
|
134
|
+
const session = opts?.session || opts?.to || "";
|
|
135
|
+
// 优先使用运行时注册的 webhook,其次使用配置的 webhook
|
|
136
|
+
const webhookUrl = getRuntimeWebhookUrl() || account?.webhookUrl;
|
|
137
|
+
if (!webhookUrl) {
|
|
138
|
+
return { channel: CHANNEL_ID, ok: false, error: new Error("HTTP API channel is passive; configure webhookUrl or register via /api/webhook/register") };
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const payload = JSON.stringify({
|
|
142
|
+
type: "text",
|
|
143
|
+
text,
|
|
144
|
+
session,
|
|
145
|
+
from: "openclaw",
|
|
146
|
+
channel: CHANNEL_ID,
|
|
147
|
+
timestamp: Date.now(),
|
|
148
|
+
});
|
|
149
|
+
const res = await fetch(webhookUrl, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: {
|
|
152
|
+
"Content-Type": "application/json",
|
|
153
|
+
"Authorization": `Bearer ${account?.token || ""}`,
|
|
154
|
+
},
|
|
155
|
+
body: payload,
|
|
156
|
+
signal: AbortSignal.timeout(10000),
|
|
157
|
+
});
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
return { channel: CHANNEL_ID, ok: false, error: new Error(`Webhook returned ${res.status}`) };
|
|
160
|
+
}
|
|
161
|
+
return { channel: CHANNEL_ID, ok: true };
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
165
|
+
return { channel: CHANNEL_ID, ok: false, error: new Error(`Webhook failed: ${msg}`) };
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
sendMedia: async (opts) => {
|
|
169
|
+
const account = opts?.account;
|
|
170
|
+
const mediaUrl = opts?.mediaUrl || "";
|
|
171
|
+
const session = opts?.session || opts?.to || "";
|
|
172
|
+
const webhookUrl = getRuntimeWebhookUrl() || account?.webhookUrl;
|
|
173
|
+
if (!webhookUrl) {
|
|
174
|
+
return { channel: CHANNEL_ID, ok: false, error: new Error("No webhookUrl configured for media delivery") };
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const payload = JSON.stringify({
|
|
178
|
+
type: "media",
|
|
179
|
+
mediaUrl,
|
|
180
|
+
session,
|
|
181
|
+
from: "openclaw",
|
|
182
|
+
channel: CHANNEL_ID,
|
|
183
|
+
timestamp: Date.now(),
|
|
184
|
+
});
|
|
185
|
+
const res = await fetch(webhookUrl, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: {
|
|
188
|
+
"Content-Type": "application/json",
|
|
189
|
+
"Authorization": `Bearer ${account?.token || ""}`,
|
|
190
|
+
},
|
|
191
|
+
body: payload,
|
|
192
|
+
signal: AbortSignal.timeout(10000),
|
|
193
|
+
});
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
return { channel: CHANNEL_ID, ok: false, error: new Error(`Webhook returned ${res.status}`) };
|
|
196
|
+
}
|
|
197
|
+
return { channel: CHANNEL_ID, ok: true };
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
201
|
+
return { channel: CHANNEL_ID, ok: false, error: new Error(`Webhook media failed: ${msg}`) };
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
// ── status 适配器 ──
|
|
206
|
+
status: {
|
|
207
|
+
defaultRuntime: {
|
|
208
|
+
accountId: "default",
|
|
209
|
+
running: false,
|
|
210
|
+
connected: false,
|
|
211
|
+
lastStartAt: null,
|
|
212
|
+
lastError: null,
|
|
213
|
+
},
|
|
214
|
+
probeAccount: async ({ account }) => {
|
|
215
|
+
try {
|
|
216
|
+
const res = await fetch(`http://127.0.0.1:${account.port}/api/health`, { signal: AbortSignal.timeout(3000) });
|
|
217
|
+
const data = await res.json();
|
|
218
|
+
return { ok: data?.ok === true, latencyMs: 0 };
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return { ok: false, latencyMs: 0 };
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw 小助渠道插件 — 入口
|
|
3
|
+
*/
|
|
4
|
+
import { httpChannelPlugin, setRuntime } from "./channel.js";
|
|
5
|
+
const plugin = {
|
|
6
|
+
id: "channel-xiaozhu",
|
|
7
|
+
name: "小助",
|
|
8
|
+
description: "小助 — 通过 HTTP/SSE 接口接入 OpenClaw 对话的通用渠道",
|
|
9
|
+
register(api) {
|
|
10
|
+
setRuntime(api.runtime);
|
|
11
|
+
api.registerChannel({ plugin: httpChannelPlugin });
|
|
12
|
+
api.logger.info("[channel-xiaozhu] 小助渠道插件已注册");
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
export default plugin;
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP 服务器 — SSE 流式、速率限制、去重、allowFrom、群聊、webhook、增强健康检查
|
|
3
|
+
*/
|
|
4
|
+
import { type Server } from "node:http";
|
|
5
|
+
import type { HttpApiAccount } from "./types.js";
|
|
6
|
+
interface ServerOptions {
|
|
7
|
+
port: number;
|
|
8
|
+
token: string;
|
|
9
|
+
accountId: string;
|
|
10
|
+
account: HttpApiAccount;
|
|
11
|
+
runtime: Record<string, any>;
|
|
12
|
+
config: Record<string, unknown>;
|
|
13
|
+
log: {
|
|
14
|
+
info: (...a: unknown[]) => void;
|
|
15
|
+
warn: (...a: unknown[]) => void;
|
|
16
|
+
error: (...a: unknown[]) => void;
|
|
17
|
+
};
|
|
18
|
+
abortSignal: AbortSignal;
|
|
19
|
+
onReady?: () => void;
|
|
20
|
+
}
|
|
21
|
+
export declare function startHttpServer(opts: ServerOptions): Promise<Server>;
|
|
22
|
+
/** 获取当前运行时 webhook URL(供 outbound 适配器使用) */
|
|
23
|
+
export declare function getRuntimeWebhookUrl(): string | null;
|
|
24
|
+
export {};
|